diff --git a/README.md b/README.md index 18af3a69..f9ed7b01 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ Note that you can also add environment variables in the configuration file (i.e. | `disable-monitoring-lock` | Whether to [disable the monitoring lock](#disable-monitoring-lock) | `false` | | `web.address` | Address to listen on | `0.0.0.0` | | `web.port` | Port to listen on | `8080` | +| `web.context-root` | Context root which should be used by the web frontent | `/` | For Kubernetes configuration, see [Kubernetes](#kubernetes-alpha) diff --git a/config/config.go b/config/config.go index 1864f1be..95448400 100644 --- a/config/config.go +++ b/config/config.go @@ -28,6 +28,9 @@ const ( // DefaultPort is the default port the service will listen on DefaultPort = 8080 + + // DefaultContextRoot is the default context root of the web application + DefaultContextRoot = "/" ) var ( diff --git a/config/web.go b/config/web.go index 2a1077eb..f1872021 100644 --- a/config/web.go +++ b/config/web.go @@ -3,6 +3,8 @@ package config import ( "fmt" "math" + "net/url" + "strings" ) // webConfig is the structure which supports the configuration of the endpoint @@ -13,6 +15,10 @@ type webConfig struct { // Port to listen on (default to 8080 specified by DefaultPort) Port int `yaml:"port"` + + ContextRoot string `yaml:"context-root"` + + safeContextRoot string } // validateAndSetDefaults checks and sets missing values based on the defaults @@ -26,9 +32,43 @@ func (web *webConfig) validateAndSetDefaults() { } else if web.Port < 0 || web.Port > math.MaxUint16 { panic(fmt.Sprintf("port has an invalid: value should be between %d and %d", 0, math.MaxUint16)) } + if len(web.ContextRoot) == 0 { + web.safeContextRoot = DefaultContextRoot + } else { + // url.PathEscape escapes all "/", in order to build a secure path + // (1) split into path fragements using "/" as delimiter + // (2) use url.PathEscape() on each fragment + // (3) re-concatinate the path using "/" as join character + const splitJoinChar = "/" + pathes := strings.Split(web.ContextRoot, splitJoinChar) + escapedPathes := make([]string, len(pathes)) + for i, path := range pathes { + escapedPathes[i] = url.PathEscape(path) + } + + web.safeContextRoot = strings.Join(escapedPathes, splitJoinChar) + + // assure that we have still a valid url + _, err := url.Parse(web.safeContextRoot) + if err != nil { + panic(fmt.Sprintf("Invalid context root %s - Error %s", web.ContextRoot, err)) + } + } } // SocketAddress returns the combination of the Address and the Port func (web *webConfig) SocketAddress() string { return fmt.Sprintf("%s:%d", web.Address, web.Port) } + +// CtxRoot returns the context root +func (web *webConfig) CtxRoot() string { + return web.safeContextRoot +} + +// AppendToCtxRoot appends the given string to the context root +// AppendToCtxRoot takes care of having only one "/" character at +// the join point and exactly on "/" at the end +func (web *webConfig) AppendToCtxRoot(fragment string) string { + return strings.TrimSuffix(web.safeContextRoot, "/") + "/" + strings.Trim(fragment, "/") + "/" +} diff --git a/config/web_test.go b/config/web_test.go index b5e6fe86..1218d031 100644 --- a/config/web_test.go +++ b/config/web_test.go @@ -11,3 +11,68 @@ func TestWebConfig_SocketAddress(t *testing.T) { t.Errorf("expected %s, got %s", "0.0.0.0:8081", web.SocketAddress()) } } + +func TestWebConfig_ContextRoot(t *testing.T) { + const expected = "/status/" + + web := &webConfig{ + ContextRoot: "/status/", + } + + web.validateAndSetDefaults() + + if web.CtxRoot() != expected { + t.Errorf("expected %s, got %s", expected, web.CtxRoot()) + } +} + +func TestWebConfig_ContextRootWithEscapableChars(t *testing.T) { + const expected = "/s%3F=ta%20t%20u&s/" + + web := &webConfig{ + ContextRoot: "/s?=ta t u&s/", + } + + web.validateAndSetDefaults() + + if web.CtxRoot() != expected { + t.Errorf("expected %s, got %s", expected, web.CtxRoot()) + } +} + +func TestWebConfig_ContextRootMultiPath(t *testing.T) { + const expected = "/app/status" + web := &webConfig{ + ContextRoot: "/app/status", + } + + web.validateAndSetDefaults() + + if web.CtxRoot() != expected { + t.Errorf("expected %s, got %s", expected, web.CtxRoot()) + } +} + +func TestWebConfig_ContextRootAppendWithEmptyContextRoot(t *testing.T) { + const expected = "/bla/" + web := &webConfig{} + + web.validateAndSetDefaults() + + if web.AppendToCtxRoot("/bla/") != expected { + t.Errorf("expected %s, got %s", expected, web.AppendToCtxRoot("/bla/")) + } +} + +func TestWebConfig_ContextRootAppendWithContext(t *testing.T) { + const expected = "/app/status/bla/" + web := &webConfig{ + ContextRoot: "/app/status", + } + + web.validateAndSetDefaults() + + if web.AppendToCtxRoot("/bla/") != expected { + t.Errorf("expected %s, got %s", expected, web.AppendToCtxRoot("/bla/")) + } +} diff --git a/main.go b/main.go index 4d96ef5b..be8bbef3 100644 --- a/main.go +++ b/main.go @@ -29,13 +29,16 @@ func main() { if cfg.Security != nil && cfg.Security.IsValid() { resultsHandler = security.Handler(serviceResultsHandler, cfg.Security) } - http.HandleFunc("/api/v1/results", resultsHandler) - http.HandleFunc("/health", healthHandler) - http.Handle("/", GzipHandler(http.FileServer(http.Dir("./static")))) + // favicon needs to be always served from the root + http.HandleFunc("/favicon.ico", favIconHandler) + http.HandleFunc(cfg.Web.AppendToCtxRoot("/api/v1/results"), resultsHandler) + http.HandleFunc(cfg.Web.AppendToCtxRoot("/health"), healthHandler) + http.Handle(cfg.Web.CtxRoot(), GzipHandler(http.StripPrefix(cfg.Web.CtxRoot(), http.FileServer(http.Dir("./static"))))) + if cfg.Metrics { - http.Handle("/metrics", promhttp.Handler()) + http.Handle(cfg.Web.AppendToCtxRoot("/metrics"), promhttp.Handler()) } - log.Printf("[main][main] Listening on %s\n", cfg.Web.SocketAddress()) + log.Printf("[main][main] Listening on %s%s\n", cfg.Web.SocketAddress(), cfg.Web.CtxRoot()) go watchdog.Monitor(cfg) log.Fatal(http.ListenAndServe(cfg.Web.SocketAddress(), nil)) } @@ -88,3 +91,8 @@ func healthHandler(writer http.ResponseWriter, _ *http.Request) { writer.WriteHeader(http.StatusOK) _, _ = writer.Write([]byte("{\"status\":\"UP\"}")) } + +// favIconHanlder responds to /favicon.ico requests +func favIconHandler(writer http.ResponseWriter, request *http.Request) { + http.ServeFile(writer, request, "./static/favicon.ico") +} diff --git a/static/index.html b/static/index.html index 859b1053..7ada745a 100644 --- a/static/index.html +++ b/static/index.html @@ -3,7 +3,7 @@