forked from mystiq/dex
use go 1.16 new package io/fs
Unify the interface for reading web statics. Now it could read an OS directory or get the content on live One could use //go:embed static var webFiles embed.FS anywhere and config dex server to take the file system by setting WebConfig{WebFS: webFiles} Signed-off-by: Rui Yang <ruiya@vmware.com> Co-authored-by: Aidan Oldershaw <aoldershaw@pivotal.io>
This commit is contained in:
parent
7b50cbf0ac
commit
4e569024fd
11 changed files with 243 additions and 128 deletions
1
Makefile
1
Makefile
|
@ -24,7 +24,6 @@ build: bin/dex
|
|||
|
||||
bin/dex:
|
||||
@mkdir -p bin/
|
||||
@go generate ./...
|
||||
@go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
|
||||
|
||||
examples: bin/grpc-client bin/example-app
|
||||
|
|
|
@ -74,7 +74,7 @@ web:
|
|||
http: 127.0.0.1:5556
|
||||
|
||||
frontend:
|
||||
dir: /web
|
||||
dir: ./web
|
||||
extra:
|
||||
foo: bar
|
||||
|
||||
|
@ -144,7 +144,7 @@ logger:
|
|||
HTTP: "127.0.0.1:5556",
|
||||
},
|
||||
Frontend: server.WebConfig{
|
||||
Dir: "/web",
|
||||
Dir: "./web",
|
||||
Extra: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
|
@ -274,6 +274,7 @@ web:
|
|||
http: 127.0.0.1:5556
|
||||
|
||||
frontend:
|
||||
dir: ./web
|
||||
extra:
|
||||
foo: bar
|
||||
|
||||
|
@ -351,7 +352,7 @@ logger:
|
|||
HTTP: "127.0.0.1:5556",
|
||||
},
|
||||
Frontend: server.WebConfig{
|
||||
Dir: "/web",
|
||||
Dir: "./web",
|
||||
Extra: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
//go:generate pkger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
1
go.mod
1
go.mod
|
@ -17,7 +17,6 @@ require (
|
|||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
|
||||
github.com/kylelemons/godebug v1.1.0
|
||||
github.com/lib/pq v1.10.0
|
||||
github.com/markbates/pkger v0.17.1
|
||||
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201219040909-8fd2afad43d1
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/oklog/run v1.1.0
|
||||
|
|
10
go.sum
10
go.sum
|
@ -1,6 +1,5 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
|
@ -15,7 +14,6 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
|
|||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AppsFlyer/go-sundheit v0.3.1 h1:Zqnr3wV3WQmXonc234k9XZAoV2KHUHw3osR5k2iHQZE=
|
||||
github.com/AppsFlyer/go-sundheit v0.3.1/go.mod h1:iZ8zWMS7idcvmqewf5mEymWWgoOiG/0WD4+aeh+heX4=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
|
@ -42,9 +40,7 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
|||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-oidc/v3 v3.0.0 h1:/mAA0XMgYJw2Uqm7WKGCsKnjitE/+A0FFbOmiRJm7LQ=
|
||||
github.com/coreos/go-oidc/v3 v3.0.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo=
|
||||
|
@ -74,7 +70,6 @@ github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8S
|
|||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
|
@ -86,8 +81,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
|||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI=
|
||||
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
||||
|
@ -189,8 +182,6 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+
|
|||
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
|
||||
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno=
|
||||
github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201219040909-8fd2afad43d1 h1:x37Q11fexMtlhecRnkdzLL6dgnS1NF1nzAJ1vic22BY=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201219040909-8fd2afad43d1/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
|
@ -487,7 +478,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -135,7 +136,7 @@ func TestHandleInvalidSAMLCallbacks(t *testing.T) {
|
|||
func TestConnectorLoginDoesNotAllowToChangeConnectorForAuthRequest(t *testing.T) {
|
||||
memStorage := memory.New(logger)
|
||||
|
||||
templates, err := loadTemplates(WebConfig{Dir: "/web"}, "/web/templates")
|
||||
templates, err := loadTemplates(webConfig{webFS: os.DirFS("../web")}, "templates")
|
||||
if err != nil {
|
||||
t.Fatal("failed to load templates")
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
@ -19,7 +20,6 @@ import (
|
|||
"github.com/felixge/httpsnoop"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/markbates/pkger"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
|
@ -101,7 +101,7 @@ type Config struct {
|
|||
|
||||
// WebConfig holds the server's frontend templates and asset configuration.
|
||||
type WebConfig struct {
|
||||
// A filepath to web static.
|
||||
// A file path to web static. If set, WebFS will be ignored.
|
||||
//
|
||||
// It is expected to contain the following directories:
|
||||
//
|
||||
|
@ -111,6 +111,10 @@ type WebConfig struct {
|
|||
//
|
||||
Dir string
|
||||
|
||||
// A file system includes web static. Will be overwritten by Dir
|
||||
// It is expected to contain the directories as Dir.
|
||||
WebFS fs.FS
|
||||
|
||||
// Defaults to "( issuer URL )/theme/logo.png"
|
||||
LogoURL string
|
||||
|
||||
|
@ -122,9 +126,6 @@ type WebConfig struct {
|
|||
|
||||
// Map of extra values passed into the templates
|
||||
Extra map[string]string
|
||||
|
||||
// Defaults to issuer URL
|
||||
HostURL string
|
||||
}
|
||||
|
||||
func value(val, defaultValue time.Duration) time.Duration {
|
||||
|
@ -207,13 +208,19 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
|
|||
supported[respType] = true
|
||||
}
|
||||
|
||||
if c.Web.Dir == "" {
|
||||
c.Web.Dir = pkger.Include("/web")
|
||||
web := webConfig{
|
||||
dir: c.Web.Dir,
|
||||
webFS: c.Web.WebFS,
|
||||
logoURL: c.Web.LogoURL,
|
||||
issuerURL: c.Issuer,
|
||||
issuer: c.Web.Issuer,
|
||||
theme: c.Web.Theme,
|
||||
extra: c.Web.Extra,
|
||||
}
|
||||
|
||||
tmpls, err := loadTemplates(c.Web, issuerURL.Path)
|
||||
static, theme, tmpls, err := loadWebConfig(web)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("server: failed to load templates: %v", err)
|
||||
return nil, fmt.Errorf("server: failed to load web static: %v", err)
|
||||
}
|
||||
|
||||
now := c.Now
|
||||
|
@ -343,11 +350,8 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
|
|||
fmt.Fprintf(w, "Health check passed")
|
||||
}))
|
||||
|
||||
staticDir := path.Join(c.Web.Dir, "static")
|
||||
themeDir := path.Join(c.Web.Dir, "themes", c.Web.Theme)
|
||||
handlePrefix("/static", http.FileServer(pkger.Dir(staticDir)))
|
||||
handlePrefix(path.Join("/themes", c.Web.Theme), http.FileServer(pkger.Dir(themeDir)))
|
||||
|
||||
handlePrefix("/static", static)
|
||||
handlePrefix("/theme", theme)
|
||||
s.mux = r
|
||||
|
||||
s.startKeyRotation(ctx, rotationStrategy, now)
|
||||
|
|
|
@ -93,7 +93,7 @@ func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Confi
|
|||
Issuer: s.URL,
|
||||
Storage: memory.New(logger),
|
||||
Web: WebConfig{
|
||||
Dir: "/web",
|
||||
Dir: "../web",
|
||||
},
|
||||
Logger: logger,
|
||||
PrometheusRegistry: prometheus.NewRegistry(),
|
||||
|
@ -132,7 +132,7 @@ func newTestServerMultipleConnectors(ctx context.Context, t *testing.T, updateCo
|
|||
Issuer: s.URL,
|
||||
Storage: memory.New(logger),
|
||||
Web: WebConfig{
|
||||
Dir: "/web",
|
||||
Dir: "../web",
|
||||
},
|
||||
Logger: logger,
|
||||
PrometheusRegistry: prometheus.NewRegistry(),
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/markbates/pkger"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -22,10 +22,18 @@ const (
|
|||
tmplError = "error.html"
|
||||
tmplDevice = "device.html"
|
||||
tmplDeviceSuccess = "device_success.html"
|
||||
tmplHeader = "header.html"
|
||||
tmplFooter = "footer.html"
|
||||
)
|
||||
|
||||
var requiredTmpls = []string{
|
||||
tmplApproval,
|
||||
tmplLogin,
|
||||
tmplPassword,
|
||||
tmplOOB,
|
||||
tmplError,
|
||||
tmplDevice,
|
||||
tmplDeviceSuccess,
|
||||
}
|
||||
|
||||
type templates struct {
|
||||
loginTmpl *template.Template
|
||||
approvalTmpl *template.Template
|
||||
|
@ -36,117 +44,182 @@ type templates struct {
|
|||
deviceSuccessTmpl *template.Template
|
||||
}
|
||||
|
||||
// loadTemplates parses the expected templates from the provided directory.
|
||||
func loadTemplates(c WebConfig, issuerPath string) (*templates, error) {
|
||||
type webConfig struct {
|
||||
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
|
||||
if c.Theme == "coreos" || c.Theme == "tectonic" {
|
||||
c.Theme = ""
|
||||
if c.theme == "coreos" || c.theme == "tectonic" {
|
||||
c.theme = ""
|
||||
}
|
||||
if c.Theme == "" {
|
||||
c.Theme = "light"
|
||||
if c.theme == "" {
|
||||
c.theme = "light"
|
||||
}
|
||||
if c.issuer == "" {
|
||||
c.issuer = "dex"
|
||||
}
|
||||
if c.dir != "" {
|
||||
c.webFS = os.DirFS(c.dir)
|
||||
}
|
||||
if c.logoURL == "" {
|
||||
c.logoURL = "theme/logo.png"
|
||||
}
|
||||
|
||||
if c.Issuer == "" {
|
||||
c.Issuer = "dex"
|
||||
}
|
||||
|
||||
if c.LogoURL == "" {
|
||||
c.LogoURL = "theme/logo.png"
|
||||
}
|
||||
|
||||
hostURL := issuerPath
|
||||
if c.HostURL != "" {
|
||||
hostURL = c.HostURL
|
||||
}
|
||||
|
||||
funcs := template.FuncMap{
|
||||
"issuer": func() string { return c.Issuer },
|
||||
"logo": func() string { return c.LogoURL },
|
||||
"static": func(assetPath string) string {
|
||||
return path.Join(hostURL, "static", assetPath)
|
||||
},
|
||||
"theme": func(assetPath string) string {
|
||||
return path.Join(hostURL, "themes", c.Theme, assetPath)
|
||||
},
|
||||
"lower": strings.ToLower,
|
||||
"extra": func(k string) string { return c.Extra[k] },
|
||||
}
|
||||
|
||||
group := template.New("")
|
||||
|
||||
// load all of our templates individually.
|
||||
// some http.FilSystem implementations don't implement Readdir
|
||||
|
||||
loginTemplate, err := loadTemplate(c.Dir, tmplLogin, funcs, group)
|
||||
staticFiles, err := fs.Sub(c.webFS, "static")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, nil, fmt.Errorf("read static dir: %v", err)
|
||||
}
|
||||
|
||||
approvalTemplate, err := loadTemplate(c.Dir, tmplApproval, funcs, group)
|
||||
themeFiles, err := fs.Sub(c.webFS, filepath.Join("themes", c.theme))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, nil, fmt.Errorf("read themes dir: %v", err)
|
||||
}
|
||||
|
||||
passwordTemplate, err := loadTemplate(c.Dir, tmplPassword, funcs, group)
|
||||
static := http.FileServer(http.FS(staticFiles))
|
||||
theme := http.FileServer(http.FS(themeFiles))
|
||||
|
||||
templates, err := loadTemplates(c, "templates")
|
||||
return static, theme, templates, err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("read dir: %v", err)
|
||||
}
|
||||
|
||||
oobTemplate, err := loadTemplate(c.Dir, tmplOOB, funcs, group)
|
||||
filenames := []string{}
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
filenames = append(filenames, filepath.Join(templatesDir, file.Name()))
|
||||
}
|
||||
if len(filenames) == 0 {
|
||||
return nil, fmt.Errorf("no files in template dir %q", templatesDir)
|
||||
}
|
||||
|
||||
issuerURL, err := url.Parse(c.issuerURL)
|
||||
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{}{
|
||||
"issuer": func() string { return c.issuer },
|
||||
"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] },
|
||||
}
|
||||
|
||||
tmpls, err := template.New("").Funcs(funcs).ParseFS(c.webFS, filenames...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("parse files: %v", err)
|
||||
}
|
||||
|
||||
deviceTemplate, err := loadTemplate(c.Dir, tmplDevice, funcs, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
missingTmpls := []string{}
|
||||
for _, tmplName := range requiredTmpls {
|
||||
if tmpls.Lookup(tmplName) == nil {
|
||||
missingTmpls = append(missingTmpls, tmplName)
|
||||
}
|
||||
}
|
||||
|
||||
deviceSuccessTemplate, err := loadTemplate(c.Dir, tmplDeviceSuccess, funcs, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if len(missingTmpls) > 0 {
|
||||
return nil, fmt.Errorf("missing template(s): %s", missingTmpls)
|
||||
}
|
||||
|
||||
_, err = loadTemplate(c.Dir, tmplHeader, funcs, group)
|
||||
if err != nil {
|
||||
// we don't actually care if this template exists
|
||||
}
|
||||
|
||||
_, err = loadTemplate(c.Dir, tmplFooter, funcs, group)
|
||||
if err != nil {
|
||||
// we don't actually care if this template exists
|
||||
}
|
||||
|
||||
return &templates{
|
||||
loginTmpl: loginTemplate,
|
||||
approvalTmpl: approvalTemplate,
|
||||
passwordTmpl: passwordTemplate,
|
||||
oobTmpl: oobTemplate,
|
||||
errorTmpl: errorTemplate,
|
||||
deviceTmpl: deviceTemplate,
|
||||
deviceSuccessTmpl: deviceSuccessTemplate,
|
||||
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
|
||||
}
|
||||
|
||||
// load a template by name from the templates dir
|
||||
func loadTemplate(dir string, name string, funcs template.FuncMap, group *template.Template) (*template.Template, error) {
|
||||
file, err := pkger.Open(filepath.Join(dir, "templates", name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// relativeURL returns the URL of the asset relative to the URL of the request path.
|
||||
// The serverPath is consulted to trim any prefix due in case it is not listening
|
||||
// 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
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
splitPath := func(p string) []string {
|
||||
res := []string{}
|
||||
parts := strings.Split(path.Clean(p), "/")
|
||||
for _, part := range parts {
|
||||
if part != "" {
|
||||
res = append(res, part)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
buffer.ReadFrom(file)
|
||||
contents := buffer.String()
|
||||
stripCommonParts := func(s1, s2 []string) ([]string, []string) {
|
||||
min := len(s1)
|
||||
if len(s2) < min {
|
||||
min = len(s2)
|
||||
}
|
||||
|
||||
return group.New(name).Funcs(funcs).Parse(contents)
|
||||
splitIndex := min
|
||||
for i := 0; i < min; i++ {
|
||||
if s1[i] != s2[i] {
|
||||
splitIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return s1[splitIndex:], s2[splitIndex:]
|
||||
}
|
||||
|
||||
server, req, asset := splitPath(serverPath), splitPath(reqPath), splitPath(assetPath)
|
||||
|
||||
// Remove common prefix of request path with server path
|
||||
_, req = stripCommonParts(server, req)
|
||||
|
||||
// Remove common prefix of request path with asset path
|
||||
asset, req = stripCommonParts(asset, req)
|
||||
|
||||
// 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 relativeURL
|
||||
}
|
||||
|
||||
var scopeDescriptions = map[string]string{
|
||||
|
|
51
server/templates_test.go
Normal file
51
server/templates_test.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package server
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRelativeURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
serverPath string
|
||||
reqPath string
|
||||
assetPath string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "server-root-req-one-level-asset-two-level",
|
||||
serverPath: "/",
|
||||
reqPath: "/auth",
|
||||
assetPath: "/theme/main.css",
|
||||
expected: "theme/main.css",
|
||||
},
|
||||
{
|
||||
name: "server-one-level-req-one-level-asset-two-level",
|
||||
serverPath: "/dex",
|
||||
reqPath: "/dex/auth",
|
||||
assetPath: "/theme/main.css",
|
||||
expected: "theme/main.css",
|
||||
},
|
||||
{
|
||||
name: "server-root-req-two-level-asset-three-level",
|
||||
serverPath: "/dex",
|
||||
reqPath: "/dex/auth/connector",
|
||||
assetPath: "assets/css/main.css",
|
||||
expected: "../assets/css/main.css",
|
||||
},
|
||||
{
|
||||
name: "external-url",
|
||||
serverPath: "/dex",
|
||||
reqPath: "/dex/auth/connector",
|
||||
assetPath: "https://kubernetes.io/images/favicon.png",
|
||||
expected: "https://kubernetes.io/images/favicon.png",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := relativeURL(test.serverPath, test.reqPath, test.assetPath)
|
||||
if actual != test.expected {
|
||||
t.Fatalf("Got '%s'. Expected '%s'", actual, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -5,17 +5,16 @@
|
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>{{ issuer }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="{{ static "main.css" }}" rel="stylesheet">
|
||||
<link href="{{ theme "styles.css" }}" rel="stylesheet">
|
||||
<link rel="icon" href="{{ theme "favicon.png" }}">
|
||||
<link href="{{ url .ReqPath "static/main.css" }}" rel="stylesheet">
|
||||
<link href="{{ url .ReqPath "theme/styles.css" }}" rel="stylesheet">
|
||||
<link rel="icon" href="{{ url .ReqPath "theme/favicon.png" }}">
|
||||
</head>
|
||||
|
||||
<body class="theme-body">
|
||||
<div class="theme-navbar">
|
||||
<div class="theme-navbar__logo-wrap">
|
||||
<img class="theme-navbar__logo" src="{{ logo }}">
|
||||
<img class="theme-navbar__logo" src="{{ url .ReqPath logo }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dex-container">
|
||||
|
||||
|
|
Loading…
Reference in a new issue