@ -1,17 +1,17 @@
package server
package server
import (
import (
"bytes"
"fmt"
"fmt"
"html/template"
"html/template"
"io"
"io"
"io/fs"
"net/http"
"net/http"
"net/url"
"os"
"path"
"path"
"path/filepath"
"path/filepath"
"sort"
"sort"
"strings"
"strings"
"github.com/markbates/pkger"
)
)
const (
const (
@ -22,10 +22,18 @@ const (
tmplError = "error.html"
tmplError = "error.html"
tmplDevice = "device.html"
tmplDevice = "device.html"
tmplDeviceSuccess = "device_success.html"
tmplDeviceSuccess = "device_success.html"
tmplHeader = "header.html"
tmplFooter = "footer.html"
)
)
var requiredTmpls = [ ] string {
tmplApproval ,
tmplLogin ,
tmplPassword ,
tmplOOB ,
tmplError ,
tmplDevice ,
tmplDeviceSuccess ,
}
type templates struct {
type templates struct {
loginTmpl * template . Template
loginTmpl * template . Template
approvalTmpl * template . Template
approvalTmpl * template . Template
@ -36,117 +44,182 @@ type templates struct {
deviceSuccessTmpl * template . Template
deviceSuccessTmpl * template . Template
}
}
// loadTemplates parses the expected templates from the provided directory.
type webConfig struct {
func loadTemplates ( c WebConfig , issuerPath string ) ( * templates , error ) {
dir string
webFS fs . FS
logoURL string
issuer string
theme string
issuerURL string
extra map [ string ] string
}
// loadWebConfig returns static assets, theme assets, and templates used by the frontend by
// reading the dir specified in the webConfig. If directory is not specified it will
// use the file system specified by webFS.
//
// The directory layout is expected to be:
//
// ( web directory )
// |- static
// |- themes
// | |- (theme name)
// |- templates
//
func loadWebConfig ( c webConfig ) ( http . Handler , http . Handler , * templates , error ) {
// fallback to the default theme if the legacy theme name is provided
// fallback to the default theme if the legacy theme name is provided
if c . Theme == "coreos" || c . Theme == "tectonic" {
if c . t heme == "coreos" || c . t heme == "tectonic" {
c . Theme = ""
c . t heme = ""
}
}
if c . Theme == "" {
if c . t heme == "" {
c . Theme = "light"
c . t heme = "light"
}
}
if c . issuer == "" {
if c . Issuer == "" {
c . issuer = "dex"
c . Issuer = "dex"
}
}
if c . dir != "" {
if c . LogoURL == "" {
c . webFS = os . DirFS ( c . dir )
c . LogoURL = "theme/logo.png"
}
}
if c . logoURL == "" {
hostURL := issuerPath
c . logoURL = "theme/logo.png"
if c . HostURL != "" {
hostURL = c . HostURL
}
}
funcs := template . FuncMap {
staticFiles , err := fs . Sub ( c . webFS , "static" )
"issuer" : func ( ) string { return c . Issuer } ,
if err != nil {
"logo" : func ( ) string { return c . LogoURL } ,
return nil , nil , nil , fmt . Errorf ( "read static dir: %v" , err )
"static" : func ( assetPath string ) string {
}
return path . Join ( hostURL , "static" , assetPath )
themeFiles , err := fs . Sub ( c . webFS , filepath . Join ( "themes" , c . theme ) )
} ,
if err != nil {
"theme" : func ( assetPath string ) string {
return nil , nil , nil , fmt . Errorf ( "read themes dir: %v" , err )
return path . Join ( hostURL , "themes" , c . Theme , assetPath )
} ,
"lower" : strings . ToLower ,
"extra" : func ( k string ) string { return c . Extra [ k ] } ,
}
}
group := template . New ( "" )
static := http . FileServer ( http . FS ( staticFiles ) )
theme := http . FileServer ( http . FS ( themeFiles ) )
// load all of our templates individually.
templates , err := loadTemplates ( c , "templates" )
// some http.FilSystem implementations don't implement Readdir
return static , theme , templates , err
}
loginTemplate , err := loadTemplate ( c . Dir , tmplLogin , funcs , group )
// loadTemplates parses the expected templates from the provided directory.
func loadTemplates ( c webConfig , templatesDir string ) ( * templates , error ) {
files , err := fs . ReadDir ( c . webFS , templatesDir )
if err != nil {
if err != nil {
return nil , err
return nil , fmt . Errorf ( "read dir: %v" , err )
}
}
approvalTemplate , err := loadTemplate ( c . Dir , tmplApproval , funcs , group )
filenames := [ ] string { }
if err != nil {
for _ , file := range files {
return nil , err
if file . IsDir ( ) {
continue
}
filenames = append ( filenames , filepath . Join ( templatesDir , file . Name ( ) ) )
}
}
if len ( filenames ) == 0 {
passwordTemplate , err := loadTemplate ( c . Dir , tmplPassword , funcs , group )
return nil , fmt . Errorf ( "no files in template dir %q" , templatesDir )
if err != nil {
return nil , err
}
}
oobTemplate , err := loadTemplate ( c . Dir , tmplOOB , funcs , group )
issuerURL , err := url . Parse ( c . issuerURL )
if err != nil {
if err != nil {
return nil , err
return nil , fmt . Errorf ( " error parsing issuerURL: %v" , err )
}
}
errorTemplate , err := loadTemplate ( c . Dir , tmplError , funcs , group )
funcs := map [ string ] interface { } {
if err != nil {
"issuer" : func ( ) string { return c . issuer } ,
return nil , err
"logo" : func ( ) string { return c . logoURL } ,
"url" : func ( reqPath , assetPath string ) string { return relativeURL ( issuerURL . Path , reqPath , assetPath ) } ,
"lower" : strings . ToLower ,
"extra" : func ( k string ) string { return c . extra [ k ] } ,
}
}
deviceTemplate , err := loadTemplate ( c . Dir , tmplDevice , funcs , group )
tmpls , err := template . New ( "" ) . Funcs ( funcs ) . ParseFS ( c . webFS , filenames ... )
if err != nil {
if err != nil {
return nil , err
return nil , fmt . Errorf ( "parse files: %v" , err )
}
}
missingTmpls := [ ] string { }
deviceSuccessTemplate , err := loadTemplate ( c . Dir , tmplDeviceSuccess , funcs , group )
for _ , tmplName := range requiredTmpls {
if err != nil {
if tmpls . Lookup ( tmplName ) == nil {
return nil , err
missingTmpls = append ( missingTmpls , tmplName )
}
}
}
if len ( missingTmpls ) > 0 {
return nil , fmt . Errorf ( "missing template(s): %s" , missingTmpls )
}
return & templates {
loginTmpl : tmpls . Lookup ( tmplLogin ) ,
approvalTmpl : tmpls . Lookup ( tmplApproval ) ,
passwordTmpl : tmpls . Lookup ( tmplPassword ) ,
oobTmpl : tmpls . Lookup ( tmplOOB ) ,
errorTmpl : tmpls . Lookup ( tmplError ) ,
deviceTmpl : tmpls . Lookup ( tmplDevice ) ,
deviceSuccessTmpl : tmpls . Lookup ( tmplDeviceSuccess ) ,
} , nil
}
_ , err = loadTemplate ( c . Dir , tmplHeader , funcs , group )
// relativeURL returns the URL of the asset relative to the URL of the request path.
if err != nil {
// The serverPath is consulted to trim any prefix due in case it is not listening
// we don't actually care if this template exists
// to the root path.
//
// Algorithm:
// 1. Remove common prefix of serverPath and reqPath
// 2. Remove common prefix of assetPath and reqPath
// 3. For each part of reqPath remaining(minus one), go up one level (..)
// 4. For each part of assetPath remaining, append it to result
//
// eg
// server listens at localhost/dex so serverPath is dex
// reqPath is /dex/auth
// assetPath is static/main.css
// relativeURL("/dex", "/dex/auth", "static/main.css") = "../static/main.css"
func relativeURL ( serverPath , reqPath , assetPath string ) string {
if u , err := url . ParseRequestURI ( assetPath ) ; err == nil && u . Scheme != "" {
// assetPath points to the external URL, no changes needed
return assetPath
}
}
_ , err = loadTemplate ( c . Dir , tmplFooter , funcs , group )
splitPath := func ( p string ) [ ] string {
if err != nil {
res := [ ] string { }
// we don't actually care if this template exists
parts := strings . Split ( path . Clean ( p ) , "/" )
for _ , part := range parts {
if part != "" {
res = append ( res , part )
}
}
return res
}
}
return & templates {
stripCommonParts := func ( s1 , s2 [ ] string ) ( [ ] string , [ ] string ) {
loginTmpl : loginTemplate ,
min := len ( s1 )
approvalTmpl : approvalTemplate ,
if len ( s2 ) < min {
passwordTmpl : passwordTemplate ,
min = len ( s2 )
oobTmpl : oobTemplate ,
}
errorTmpl : errorTemplate ,
deviceTmpl : deviceTemplate ,
deviceSuccessTmpl : deviceSuccessTemplate ,
} , nil
}
// load a template by name from the templates dir
splitIndex := min
func loadTemplate ( dir string , name string , funcs template . FuncMap , group * template . Template ) ( * template . Template , error ) {
for i := 0 ; i < min ; i ++ {
file , err := pkger . Open ( filepath . Join ( dir , "templates" , name ) )
if s1 [ i ] != s2 [ i ] {
if err != nil {
splitIndex = i
return nil , err
break
}
}
return s1 [ splitIndex : ] , s2 [ splitIndex : ]
}
}
defer file . Close ( )
server , req , asset := splitPath ( serverPath ) , splitPath ( reqPath ) , splitPath ( assetPath )
// Remove common prefix of request path with server path
_ , req = stripCommonParts ( server , req )
var buffer bytes . Buffer
// Remove common prefix of request path with asset path
buffer . ReadFrom ( file )
asset , req = stripCommonParts ( asset , req )
contents := buffer . String ( )
// For each part of the request remaining (minus one) -> go up one level (..)
// For each part of the asset remaining -> append it
var relativeURL string
for i := 0 ; i < len ( req ) - 1 ; i ++ {
relativeURL = path . Join ( ".." , relativeURL )
}
relativeURL = path . Join ( relativeURL , path . Join ( asset ... ) )
return group . New ( name ) . Funcs ( funcs ) . Parse ( contents )
return relativeURL
}
}
var scopeDescriptions = map [ string ] string {
var scopeDescriptions = map [ string ] string {