Merge pull request #1554 from yanniszark/feature-web-templates-use-relative-urls

server: templates: use relative URLs to refer to assets
This commit is contained in:
Joel Speed 2019-10-03 10:49:18 +01:00 committed by GitHub
commit 4bede5eb80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 182 additions and 63 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
bin bin
dist dist
_output _output
.idea

View file

@ -101,7 +101,7 @@ func (h *healthChecker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mu.RUnlock() h.mu.RUnlock()
if err != nil { if err != nil {
h.s.renderError(w, http.StatusInternalServerError, "Health check failed.") h.s.renderError(r, w, http.StatusInternalServerError, "Health check failed.")
return return
} }
fmt.Fprintf(w, "Health check passed in %s", t) fmt.Fprintf(w, "Health check passed in %s", t)
@ -112,13 +112,13 @@ func (s *Server) handlePublicKeys(w http.ResponseWriter, r *http.Request) {
keys, err := s.storage.GetKeys() keys, err := s.storage.GetKeys()
if err != nil { if err != nil {
s.logger.Errorf("failed to get keys: %v", err) s.logger.Errorf("failed to get keys: %v", err)
s.renderError(w, http.StatusInternalServerError, "Internal server error.") s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
return return
} }
if keys.SigningKeyPub == nil { if keys.SigningKeyPub == nil {
s.logger.Errorf("No public keys found.") s.logger.Errorf("No public keys found.")
s.renderError(w, http.StatusInternalServerError, "Internal server error.") s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
return return
} }
@ -133,7 +133,7 @@ func (s *Server) handlePublicKeys(w http.ResponseWriter, r *http.Request) {
data, err := json.MarshalIndent(jwks, "", " ") data, err := json.MarshalIndent(jwks, "", " ")
if err != nil { if err != nil {
s.logger.Errorf("failed to marshal discovery data: %v", err) s.logger.Errorf("failed to marshal discovery data: %v", err)
s.renderError(w, http.StatusInternalServerError, "Internal server error.") s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
return return
} }
maxAge := keys.NextRotation.Sub(s.now()) maxAge := keys.NextRotation.Sub(s.now())
@ -214,7 +214,7 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
status = err.Status() status = err.Status()
} }
s.renderError(w, status, err.Error()) s.renderError(r, w, status, err.Error())
return return
} }
@ -226,14 +226,14 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
authReq.Expiry = s.now().Add(s.authRequestsValidFor) authReq.Expiry = s.now().Add(s.authRequestsValidFor)
if err := s.storage.CreateAuthRequest(*authReq); err != nil { if err := s.storage.CreateAuthRequest(*authReq); err != nil {
s.logger.Errorf("Failed to create authorization request: %v", err) s.logger.Errorf("Failed to create authorization request: %v", err)
s.renderError(w, http.StatusInternalServerError, "Failed to connect to the database.") s.renderError(r, w, http.StatusInternalServerError, "Failed to connect to the database.")
return return
} }
connectors, err := s.storage.ListConnectors() connectors, err := s.storage.ListConnectors()
if err != nil { if err != nil {
s.logger.Errorf("Failed to get list of connectors: %v", err) s.logger.Errorf("Failed to get list of connectors: %v", err)
s.renderError(w, http.StatusInternalServerError, "Failed to retrieve connector list.") s.renderError(r, w, http.StatusInternalServerError, "Failed to retrieve connector list.")
return return
} }
@ -271,7 +271,7 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
i++ i++
} }
if err := s.templates.login(w, connectorInfos); err != nil { if err := s.templates.login(r, w, connectorInfos, r.URL.Path); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
} }
@ -281,7 +281,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
conn, err := s.getConnector(connID) conn, err := s.getConnector(connID)
if err != nil { if err != nil {
s.logger.Errorf("Failed to create authorization request: %v", err) s.logger.Errorf("Failed to create authorization request: %v", err)
s.renderError(w, http.StatusBadRequest, "Requested resource does not exist") s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist")
return return
} }
@ -291,9 +291,9 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
s.logger.Errorf("Failed to get auth request: %v", err) s.logger.Errorf("Failed to get auth request: %v", err)
if err == storage.ErrNotFound { if err == storage.ErrNotFound {
s.renderError(w, http.StatusBadRequest, "Login session expired.") s.renderError(r, w, http.StatusBadRequest, "Login session expired.")
} else { } else {
s.renderError(w, http.StatusInternalServerError, "Database error.") s.renderError(r, w, http.StatusInternalServerError, "Database error.")
} }
return return
} }
@ -306,7 +306,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
} }
if err := s.storage.UpdateAuthRequest(authReqID, updater); err != nil { if err := s.storage.UpdateAuthRequest(authReqID, updater); err != nil {
s.logger.Errorf("Failed to set connector ID on auth request: %v", err) s.logger.Errorf("Failed to set connector ID on auth request: %v", err)
s.renderError(w, http.StatusInternalServerError, "Database error.") s.renderError(r, w, http.StatusInternalServerError, "Database error.")
return return
} }
} }
@ -324,19 +324,19 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
callbackURL, err := conn.LoginURL(scopes, s.absURL("/callback"), authReqID) callbackURL, err := conn.LoginURL(scopes, s.absURL("/callback"), authReqID)
if err != nil { if err != nil {
s.logger.Errorf("Connector %q returned error when creating callback: %v", connID, err) s.logger.Errorf("Connector %q returned error when creating callback: %v", connID, err)
s.renderError(w, http.StatusInternalServerError, "Login error.") s.renderError(r, w, http.StatusInternalServerError, "Login error.")
return return
} }
http.Redirect(w, r, callbackURL, http.StatusFound) http.Redirect(w, r, callbackURL, http.StatusFound)
case connector.PasswordConnector: case connector.PasswordConnector:
if err := s.templates.password(w, r.URL.String(), "", usernamePrompt(conn), false, showBacklink); err != nil { if err := s.templates.password(r, w, r.URL.String(), "", usernamePrompt(conn), false, showBacklink, r.URL.Path); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
case connector.SAMLConnector: case connector.SAMLConnector:
action, value, err := conn.POSTData(scopes, authReqID) action, value, err := conn.POSTData(scopes, authReqID)
if err != nil { if err != nil {
s.logger.Errorf("Creating SAML data: %v", err) s.logger.Errorf("Creating SAML data: %v", err)
s.renderError(w, http.StatusInternalServerError, "Connector Login Error") s.renderError(r, w, http.StatusInternalServerError, "Connector Login Error")
return return
} }
@ -358,12 +358,12 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
</body> </body>
</html>`, action, value, authReqID) </html>`, action, value, authReqID)
default: default:
s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.") s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
} }
case http.MethodPost: case http.MethodPost:
passwordConnector, ok := conn.Connector.(connector.PasswordConnector) passwordConnector, ok := conn.Connector.(connector.PasswordConnector)
if !ok { if !ok {
s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.") s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
return return
} }
@ -373,11 +373,11 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
identity, ok, err := passwordConnector.Login(r.Context(), scopes, username, password) identity, ok, err := passwordConnector.Login(r.Context(), scopes, username, password)
if err != nil { if err != nil {
s.logger.Errorf("Failed to login user: %v", err) s.logger.Errorf("Failed to login user: %v", err)
s.renderError(w, http.StatusInternalServerError, fmt.Sprintf("Login error: %v", err)) s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Login error: %v", err))
return return
} }
if !ok { if !ok {
if err := s.templates.password(w, r.URL.String(), username, usernamePrompt(passwordConnector), true, showBacklink); err != nil { if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(passwordConnector), true, showBacklink, r.URL.Path); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
return return
@ -385,13 +385,13 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector) redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector)
if err != nil { if err != nil {
s.logger.Errorf("Failed to finalize login: %v", err) s.logger.Errorf("Failed to finalize login: %v", err)
s.renderError(w, http.StatusInternalServerError, "Login error.") s.renderError(r, w, http.StatusInternalServerError, "Login error.")
return return
} }
http.Redirect(w, r, redirectURL, http.StatusSeeOther) http.Redirect(w, r, redirectURL, http.StatusSeeOther)
default: default:
s.renderError(w, http.StatusBadRequest, "Unsupported request method.") s.renderError(r, w, http.StatusBadRequest, "Unsupported request method.")
} }
} }
@ -400,16 +400,16 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
switch r.Method { switch r.Method {
case http.MethodGet: // OAuth2 callback case http.MethodGet: // OAuth2 callback
if authID = r.URL.Query().Get("state"); authID == "" { if authID = r.URL.Query().Get("state"); authID == "" {
s.renderError(w, http.StatusBadRequest, "User session error.") s.renderError(r, w, http.StatusBadRequest, "User session error.")
return return
} }
case http.MethodPost: // SAML POST binding case http.MethodPost: // SAML POST binding
if authID = r.PostFormValue("RelayState"); authID == "" { if authID = r.PostFormValue("RelayState"); authID == "" {
s.renderError(w, http.StatusBadRequest, "User session error.") s.renderError(r, w, http.StatusBadRequest, "User session error.")
return return
} }
default: default:
s.renderError(w, http.StatusBadRequest, "Method not supported") s.renderError(r, w, http.StatusBadRequest, "Method not supported")
return return
} }
@ -417,24 +417,24 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
if err != nil { if err != nil {
if err == storage.ErrNotFound { if err == storage.ErrNotFound {
s.logger.Errorf("Invalid 'state' parameter provided: %v", err) s.logger.Errorf("Invalid 'state' parameter provided: %v", err)
s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.") s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
return return
} }
s.logger.Errorf("Failed to get auth request: %v", err) s.logger.Errorf("Failed to get auth request: %v", err)
s.renderError(w, http.StatusInternalServerError, "Database error.") s.renderError(r, w, http.StatusInternalServerError, "Database error.")
return return
} }
if connID := mux.Vars(r)["connector"]; connID != "" && connID != authReq.ConnectorID { if connID := mux.Vars(r)["connector"]; connID != "" && connID != authReq.ConnectorID {
s.logger.Errorf("Connector mismatch: authentication started with id %q, but callback for id %q was triggered", authReq.ConnectorID, connID) s.logger.Errorf("Connector mismatch: authentication started with id %q, but callback for id %q was triggered", authReq.ConnectorID, connID)
s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
return return
} }
conn, err := s.getConnector(authReq.ConnectorID) conn, err := s.getConnector(authReq.ConnectorID)
if err != nil { if err != nil {
s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err) s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err)
s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
return return
} }
@ -443,32 +443,32 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
case connector.CallbackConnector: case connector.CallbackConnector:
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
s.logger.Errorf("SAML request mapped to OAuth2 connector") s.logger.Errorf("SAML request mapped to OAuth2 connector")
s.renderError(w, http.StatusBadRequest, "Invalid request") s.renderError(r, w, http.StatusBadRequest, "Invalid request")
return return
} }
identity, err = conn.HandleCallback(parseScopes(authReq.Scopes), r) identity, err = conn.HandleCallback(parseScopes(authReq.Scopes), r)
case connector.SAMLConnector: case connector.SAMLConnector:
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
s.logger.Errorf("OAuth2 request mapped to SAML connector") s.logger.Errorf("OAuth2 request mapped to SAML connector")
s.renderError(w, http.StatusBadRequest, "Invalid request") s.renderError(r, w, http.StatusBadRequest, "Invalid request")
return return
} }
identity, err = conn.HandlePOST(parseScopes(authReq.Scopes), r.PostFormValue("SAMLResponse"), authReq.ID) identity, err = conn.HandlePOST(parseScopes(authReq.Scopes), r.PostFormValue("SAMLResponse"), authReq.ID)
default: default:
s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
return return
} }
if err != nil { if err != nil {
s.logger.Errorf("Failed to authenticate: %v", err) s.logger.Errorf("Failed to authenticate: %v", err)
s.renderError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to authenticate: %v", err)) s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Failed to authenticate: %v", err))
return return
} }
redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector) redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector)
if err != nil { if err != nil {
s.logger.Errorf("Failed to finalize login: %v", err) s.logger.Errorf("Failed to finalize login: %v", err)
s.renderError(w, http.StatusInternalServerError, "Login error.") s.renderError(r, w, http.StatusInternalServerError, "Login error.")
return return
} }
@ -511,12 +511,12 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
authReq, err := s.storage.GetAuthRequest(r.FormValue("req")) authReq, err := s.storage.GetAuthRequest(r.FormValue("req"))
if err != nil { if err != nil {
s.logger.Errorf("Failed to get auth request: %v", err) s.logger.Errorf("Failed to get auth request: %v", err)
s.renderError(w, http.StatusInternalServerError, "Database error.") s.renderError(r, w, http.StatusInternalServerError, "Database error.")
return return
} }
if !authReq.LoggedIn { if !authReq.LoggedIn {
s.logger.Errorf("Auth request does not have an identity for approval") s.logger.Errorf("Auth request does not have an identity for approval")
s.renderError(w, http.StatusInternalServerError, "Login process not yet finalized.") s.renderError(r, w, http.StatusInternalServerError, "Login process not yet finalized.")
return return
} }
@ -529,15 +529,15 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
client, err := s.storage.GetClient(authReq.ClientID) client, err := s.storage.GetClient(authReq.ClientID)
if err != nil { if err != nil {
s.logger.Errorf("Failed to get client %q: %v", authReq.ClientID, err) s.logger.Errorf("Failed to get client %q: %v", authReq.ClientID, err)
s.renderError(w, http.StatusInternalServerError, "Failed to retrieve client.") s.renderError(r, w, http.StatusInternalServerError, "Failed to retrieve client.")
return return
} }
if err := s.templates.approval(w, authReq.ID, authReq.Claims.Username, client.Name, authReq.Scopes); err != nil { if err := s.templates.approval(r, w, authReq.ID, authReq.Claims.Username, client.Name, authReq.Scopes, r.URL.Path); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
case http.MethodPost: case http.MethodPost:
if r.FormValue("approval") != "approve" { if r.FormValue("approval") != "approve" {
s.renderError(w, http.StatusInternalServerError, "Approval rejected.") s.renderError(r, w, http.StatusInternalServerError, "Approval rejected.")
return return
} }
s.sendCodeResponse(w, r, authReq) s.sendCodeResponse(w, r, authReq)
@ -546,22 +546,22 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authReq storage.AuthRequest) { func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authReq storage.AuthRequest) {
if s.now().After(authReq.Expiry) { if s.now().After(authReq.Expiry) {
s.renderError(w, http.StatusBadRequest, "User session has expired.") s.renderError(r, w, http.StatusBadRequest, "User session has expired.")
return return
} }
if err := s.storage.DeleteAuthRequest(authReq.ID); err != nil { if err := s.storage.DeleteAuthRequest(authReq.ID); err != nil {
if err != storage.ErrNotFound { if err != storage.ErrNotFound {
s.logger.Errorf("Failed to delete authorization request: %v", err) s.logger.Errorf("Failed to delete authorization request: %v", err)
s.renderError(w, http.StatusInternalServerError, "Internal server error.") s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
} else { } else {
s.renderError(w, http.StatusBadRequest, "User session error.") s.renderError(r, w, http.StatusBadRequest, "User session error.")
} }
return return
} }
u, err := url.Parse(authReq.RedirectURI) u, err := url.Parse(authReq.RedirectURI)
if err != nil { if err != nil {
s.renderError(w, http.StatusInternalServerError, "Invalid redirect URI.") s.renderError(r, w, http.StatusInternalServerError, "Invalid redirect URI.")
return return
} }
@ -598,14 +598,14 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
} }
if err := s.storage.CreateAuthCode(code); err != nil { if err := s.storage.CreateAuthCode(code); err != nil {
s.logger.Errorf("Failed to create auth code: %v", err) s.logger.Errorf("Failed to create auth code: %v", err)
s.renderError(w, http.StatusInternalServerError, "Internal server error.") s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
return return
} }
// Implicit and hybrid flows that try to use the OOB redirect URI are // Implicit and hybrid flows that try to use the OOB redirect URI are
// rejected earlier. If we got here we're using the code flow. // rejected earlier. If we got here we're using the code flow.
if authReq.RedirectURI == redirectURIOOB { if authReq.RedirectURI == redirectURIOOB {
if err := s.templates.oob(w, code.ID); err != nil { if err := s.templates.oob(r, w, code.ID, r.URL.Path); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
return return
@ -1119,8 +1119,8 @@ func (s *Server) writeAccessToken(w http.ResponseWriter, idToken, accessToken, r
w.Write(data) w.Write(data)
} }
func (s *Server) renderError(w http.ResponseWriter, status int, description string) { func (s *Server) renderError(r *http.Request, w http.ResponseWriter, status int, description string) {
if err := s.templates.err(w, status, description); err != nil { if err := s.templates.err(r, w, status, description); err != nil {
s.logger.Errorf("Server template error: %v", err) s.logger.Errorf("Server template error: %v", err)
} }
} }

View file

@ -6,7 +6,9 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@ -94,7 +96,7 @@ func loadWebConfig(c webConfig) (static, theme http.Handler, templates *template
c.dir = "./web" c.dir = "./web"
} }
if c.logoURL == "" { if c.logoURL == "" {
c.logoURL = join(c.issuerURL, "theme/logo.png") c.logoURL = "theme/logo.png"
} }
if err := dirExists(c.dir); err != nil { if err := dirExists(c.dir); err != nil {
@ -136,10 +138,15 @@ func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
return nil, fmt.Errorf("no files in template dir %q", templatesDir) return nil, fmt.Errorf("no files in template dir %q", templatesDir)
} }
issuerURL, err := url.Parse(c.issuerURL)
if err != nil {
return nil, fmt.Errorf("error parsing issuerURL: %v", err)
}
funcs := map[string]interface{}{ funcs := map[string]interface{}{
"issuer": func() string { return c.issuer }, "issuer": func() string { return c.issuer },
"logo": func() string { return c.logoURL }, "logo": func() string { return c.logoURL },
"url": func(s string) string { return join(c.issuerURL, s) }, "url": func(reqPath, assetPath string) string { return relativeURL(issuerURL.Path, reqPath, assetPath) },
"lower": strings.ToLower, "lower": strings.ToLower,
"extra": func(k string) string { return c.extra[k] }, "extra": func(k string) string { return c.extra[k] },
} }
@ -166,6 +173,69 @@ func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
}, nil }, nil
} }
// 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 {
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
}
stripCommonParts := func(s1, s2 []string) ([]string, []string) {
min := len(s1)
if len(s2) < min {
min = len(s2)
}
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
server, 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{ var scopeDescriptions = map[string]string{
"offline_access": "Have offline access", "offline_access": "Have offline access",
"profile": "View basic profile information", "profile": "View basic profile information",
@ -184,26 +254,28 @@ func (n byName) Len() int { return len(n) }
func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name } func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name }
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] } func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (t *templates) login(w http.ResponseWriter, connectors []connectorInfo) error { func (t *templates) login(r *http.Request, w http.ResponseWriter, connectors []connectorInfo, reqPath string) error {
sort.Sort(byName(connectors)) sort.Sort(byName(connectors))
data := struct { data := struct {
Connectors []connectorInfo Connectors []connectorInfo
}{connectors} ReqPath string
}{connectors, r.URL.Path}
return renderTemplate(w, t.loginTmpl, data) return renderTemplate(w, t.loginTmpl, data)
} }
func (t *templates) password(w http.ResponseWriter, postURL, lastUsername, usernamePrompt string, lastWasInvalid, showBacklink bool) error { func (t *templates) password(r *http.Request, w http.ResponseWriter, postURL, lastUsername, usernamePrompt string, lastWasInvalid, showBacklink bool, reqPath string) error {
data := struct { data := struct {
PostURL string PostURL string
BackLink bool BackLink bool
Username string Username string
UsernamePrompt string UsernamePrompt string
Invalid bool Invalid bool
}{postURL, showBacklink, lastUsername, usernamePrompt, lastWasInvalid} ReqPath string
}{postURL, showBacklink, lastUsername, usernamePrompt, lastWasInvalid, r.URL.Path}
return renderTemplate(w, t.passwordTmpl, data) return renderTemplate(w, t.passwordTmpl, data)
} }
func (t *templates) approval(w http.ResponseWriter, authReqID, username, clientName string, scopes []string) error { func (t *templates) approval(r *http.Request, w http.ResponseWriter, authReqID, username, clientName string, scopes []string, reqPath string) error {
accesses := []string{} accesses := []string{}
for _, scope := range scopes { for _, scope := range scopes {
access, ok := scopeDescriptions[scope] access, ok := scopeDescriptions[scope]
@ -217,23 +289,26 @@ func (t *templates) approval(w http.ResponseWriter, authReqID, username, clientN
Client string Client string
AuthReqID string AuthReqID string
Scopes []string Scopes []string
}{username, clientName, authReqID, accesses} ReqPath string
}{username, clientName, authReqID, accesses, r.URL.Path}
return renderTemplate(w, t.approvalTmpl, data) return renderTemplate(w, t.approvalTmpl, data)
} }
func (t *templates) oob(w http.ResponseWriter, code string) error { func (t *templates) oob(r *http.Request, w http.ResponseWriter, code string, reqPath string) error {
data := struct { data := struct {
Code string Code string
}{code} ReqPath string
}{code, r.URL.Path}
return renderTemplate(w, t.oobTmpl, data) return renderTemplate(w, t.oobTmpl, data)
} }
func (t *templates) err(w http.ResponseWriter, errCode int, errMsg string) error { func (t *templates) err(r *http.Request, w http.ResponseWriter, errCode int, errMsg string) error {
w.WriteHeader(errCode) w.WriteHeader(errCode)
data := struct { data := struct {
ErrType string ErrType string
ErrMsg string ErrMsg string
}{http.StatusText(errCode), errMsg} ReqPath string
}{http.StatusText(errCode), errMsg, r.URL.Path}
if err := t.errorTmpl.Execute(w, data); err != nil { if err := t.errorTmpl.Execute(w, data); err != nil {
return fmt.Errorf("Error rendering template %s: %s", t.errorTmpl.Name(), err) return fmt.Errorf("Error rendering template %s: %s", t.errorTmpl.Name(), err)
} }

View file

@ -1 +1,44 @@
package server 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",
},
}
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)
}
})
}
}

View file

@ -5,15 +5,15 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>{{ issuer }}</title> <title>{{ issuer }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{ url "static/main.css" }}" rel="stylesheet"> <link href="{{ url .ReqPath "static/main.css" }}" rel="stylesheet">
<link href="{{ url "theme/styles.css" }}" rel="stylesheet"> <link href="{{ url .ReqPath "theme/styles.css" }}" rel="stylesheet">
<link rel="icon" href="{{ url "theme/favicon.png" }}"> <link rel="icon" href="{{ url .ReqPath "theme/favicon.png" }}">
</head> </head>
<body class="theme-body"> <body class="theme-body">
<div class="theme-navbar"> <div class="theme-navbar">
<div class="theme-navbar__logo-wrap"> <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> </div>