From 8ba433a4d0ad2fd24d5c64f41abddc88744965cd Mon Sep 17 00:00:00 2001 From: Vladimir L Date: Wed, 1 Jun 2022 03:29:46 +0200 Subject: [PATCH 01/11] introduced new parameter to define HTTP path prefix, which is useful when RequestBaskets service is running behind HTTP reverse proxy, see #72 for details --- README.md | 2 +- config.go | 10 ++++++++- handlers.go | 41 +++++++++++++++++++++++++++------- server.go | 54 +++++++++++++++++++++++++-------------------- web_basket.html.go | 44 ++++++++++++++++++------------------ web_baskets.html.go | 22 +++++++++--------- web_index.html.go | 14 ++++++------ 7 files changed, 113 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 5df9069..6541952 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Usage of bin/request-baskets: -token string Master token, random token is generated if not provided -basket value - Name of a basket to auto-create during service startup (can be specified multiple times) + Name of a basket to auto-create during service startup (can be specified multiple times) ``` ### Parameters diff --git a/config.go b/config.go index 9ec3d0e..1d0f060 100644 --- a/config.go +++ b/config.go @@ -34,6 +34,7 @@ type ServerConfig struct { DbFile string DbConnection string Baskets []string + PathPrefix string } type arrayFlags []string @@ -59,12 +60,18 @@ func CreateConfig() *ServerConfig { "Baskets storage type: %s - in-memory, %s - Bolt DB, %s - SQL database", DbTypeMemory, DbTypeBolt, DbTypeSQL)) var dbFile = flag.String("file", "./baskets.db", "Database location, only applicable for file or SQL databases") var dbConnection = flag.String("conn", "", "Database connection string for SQL databases, if undefined \"file\" argument is considered") + var prefix = flag.String("prefix", "", "Service HTTP path prefix") var baskets arrayFlags flag.Var(&baskets, "basket", "Name of a basket to auto-create during service startup (can be specified multiple times)") flag.Parse() + pathPrefix := *prefix + if (len(pathPrefix) > 0) && (pathPrefix[0:1] != "/") { + pathPrefix = "/" + pathPrefix + } + var token = *masterToken if len(token) == 0 { token, _ = GenerateToken() @@ -81,5 +88,6 @@ func CreateConfig() *ServerConfig { DbType: *dbType, DbFile: *dbFile, DbConnection: *dbConnection, - Baskets: baskets} + Baskets: baskets, + PathPrefix: pathPrefix} } diff --git a/handlers.go b/handlers.go index 5790c61..beb6ae9 100644 --- a/handlers.go +++ b/handlers.go @@ -343,13 +343,20 @@ func ClearBasket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { // ForwardToWeb handels HTTP forwarding to /web func ForwardToWeb(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - http.Redirect(w, r, "/"+serviceUIPath, http.StatusFound) + http.Redirect(w, r, pathPrefix+"/"+serviceUIPath, http.StatusFound) +} + +type TemplateData struct { + Prefix string + Version *Version + Basket string + Data interface{} } // WebIndexPage handles HTTP request to render index page func WebIndexPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { w.Header().Set("Content-Type", "text/html; charset=utf-8") - indexPageTemplate.Execute(w, version) + indexPageTemplate.Execute(w, TemplateData{Prefix: pathPrefix, Version: version}) } // WebBasketPage handles HTTP request to render basket details page @@ -359,9 +366,9 @@ func WebBasketPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) case serviceOldAPIPath: // admin page to access all baskets w.Header().Set("Content-Type", "text/html; charset=utf-8") - basketsPageTemplate.Execute(w, version) + basketsPageTemplate.Execute(w, TemplateData{Prefix: pathPrefix, Version: version}) default: - basketPageTemplate.Execute(w, name) + basketPageTemplate.Execute(w, TemplateData{Prefix: pathPrefix, Version: version, Basket: name}) } } else { http.Error(w, "Basket name does not match pattern: "+validBasketName.String(), http.StatusBadRequest) @@ -370,10 +377,10 @@ func WebBasketPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) // AcceptBasketRequests accepts and handles HTTP requests passed to different baskets func AcceptBasketRequests(w http.ResponseWriter, r *http.Request) { - name := strings.Split(r.URL.Path, "/")[1] - - if !validBasketName.MatchString(name) { - http.Error(w, "invalid basket name; the name does not match pattern: "+validBasketName.String(), http.StatusBadRequest) + name, err := getBasketName(r) + if err != nil { + log.Printf("[error] %s", err) + http.Error(w, err.Error(), http.StatusBadRequest) } else if basket := basketsDb.Get(name); basket != nil { request := basket.Add(r) @@ -394,6 +401,24 @@ func AcceptBasketRequests(w http.ResponseWriter, r *http.Request) { } } +func getBasketName(r *http.Request) (string, error) { + path := r.URL.Path + if len(pathPrefix) > 0 { + if strings.HasPrefix(path, pathPrefix) { + path = strings.TrimPrefix(path, pathPrefix) + } else { + return "", fmt.Errorf("incoming request is outside of configured path prefix: %s", pathPrefix) + } + } + + name := strings.Split(path, "/")[1] + if !validBasketName.MatchString(name) { + return "", fmt.Errorf("invalid basket name; the name does not match pattern: %s", validBasketName.String()) + } + + return name, nil +} + func forwardAndForget(request *RequestData, config BasketConfig, name string) { // forward request and discard the response response, err := request.Forward(getHTTPClient(config.InsecureTLS), config, name) diff --git a/server.go b/server.go index b4d6837..11efee4 100644 --- a/server.go +++ b/server.go @@ -16,6 +16,7 @@ var basketsDb BasketsDatabase var httpClient *http.Client var httpInsecureClient *http.Client var version *Version +var pathPrefix string // CreateServer creates an instance of Request Baskets server func CreateServer(config *ServerConfig) *http.Server { @@ -42,45 +43,50 @@ func CreateServer(config *ServerConfig) *http.Server { insecureTransport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} httpInsecureClient = &http.Client{Transport: insecureTransport} + pathPrefix = config.PathPrefix + if len(pathPrefix) > 0 { + log.Printf("[info] service path prefix: %s", pathPrefix) + } + // configure service HTTP router router := httprouter.New() //// Old API mapping //// // basket names - router.GET("/"+serviceOldAPIPath, GetBaskets) + router.GET(pathPrefix+"/"+serviceOldAPIPath, GetBaskets) // basket management - router.GET("/"+serviceOldAPIPath+"/:basket", GetBasket) - router.POST("/"+serviceOldAPIPath+"/:basket", CreateBasket) - router.PUT("/"+serviceOldAPIPath+"/:basket", UpdateBasket) - router.DELETE("/"+serviceOldAPIPath+"/:basket", DeleteBasket) - router.GET("/"+serviceOldAPIPath+"/:basket/responses/:method", GetBasketResponse) - router.PUT("/"+serviceOldAPIPath+"/:basket/responses/:method", UpdateBasketResponse) + router.GET(pathPrefix+"/"+serviceOldAPIPath+"/:basket", GetBasket) + router.POST(pathPrefix+"/"+serviceOldAPIPath+"/:basket", CreateBasket) + router.PUT(pathPrefix+"/"+serviceOldAPIPath+"/:basket", UpdateBasket) + router.DELETE(pathPrefix+"/"+serviceOldAPIPath+"/:basket", DeleteBasket) + router.GET(pathPrefix+"/"+serviceOldAPIPath+"/:basket/responses/:method", GetBasketResponse) + router.PUT(pathPrefix+"/"+serviceOldAPIPath+"/:basket/responses/:method", UpdateBasketResponse) // requests management - router.GET("/"+serviceOldAPIPath+"/:basket/requests", GetBasketRequests) - router.DELETE("/"+serviceOldAPIPath+"/:basket/requests", ClearBasket) + router.GET(pathPrefix+"/"+serviceOldAPIPath+"/:basket/requests", GetBasketRequests) + router.DELETE(pathPrefix+"/"+serviceOldAPIPath+"/:basket/requests", ClearBasket) //// New API mapping //// // service details - router.GET("/"+serviceAPIPath+"/stats", GetStats) - router.GET("/"+serviceAPIPath+"/version", GetVersion) + router.GET(pathPrefix+"/"+serviceAPIPath+"/stats", GetStats) + router.GET(pathPrefix+"/"+serviceAPIPath+"/version", GetVersion) // basket names - router.GET("/"+serviceAPIPath+"/baskets", GetBaskets) + router.GET(pathPrefix+"/"+serviceAPIPath+"/baskets", GetBaskets) // basket management - router.GET("/"+serviceAPIPath+"/baskets/:basket", GetBasket) - router.POST("/"+serviceAPIPath+"/baskets/:basket", CreateBasket) - router.PUT("/"+serviceAPIPath+"/baskets/:basket", UpdateBasket) - router.DELETE("/"+serviceAPIPath+"/baskets/:basket", DeleteBasket) - router.GET("/"+serviceAPIPath+"/baskets/:basket/responses/:method", GetBasketResponse) - router.PUT("/"+serviceAPIPath+"/baskets/:basket/responses/:method", UpdateBasketResponse) + router.GET(pathPrefix+"/"+serviceAPIPath+"/baskets/:basket", GetBasket) + router.POST(pathPrefix+"/"+serviceAPIPath+"/baskets/:basket", CreateBasket) + router.PUT(pathPrefix+"/"+serviceAPIPath+"/baskets/:basket", UpdateBasket) + router.DELETE(pathPrefix+"/"+serviceAPIPath+"/baskets/:basket", DeleteBasket) + router.GET(pathPrefix+"/"+serviceAPIPath+"/baskets/:basket/responses/:method", GetBasketResponse) + router.PUT(pathPrefix+"/"+serviceAPIPath+"/baskets/:basket/responses/:method", UpdateBasketResponse) // requests management - router.GET("/"+serviceAPIPath+"/baskets/:basket/requests", GetBasketRequests) - router.DELETE("/"+serviceAPIPath+"/baskets/:basket/requests", ClearBasket) + router.GET(pathPrefix+"/"+serviceAPIPath+"/baskets/:basket/requests", GetBasketRequests) + router.DELETE(pathPrefix+"/"+serviceAPIPath+"/baskets/:basket/requests", ClearBasket) // web pages - router.GET("/", ForwardToWeb) - router.GET("/"+serviceUIPath, WebIndexPage) - router.GET("/"+serviceUIPath+"/:basket", WebBasketPage) - //router.ServeFiles("/"+serviceUIPath+"/*filepath", http.Dir("./src/github.com/darklynx/request-baskets/web")) + router.GET(pathPrefix+"/", ForwardToWeb) + router.GET(pathPrefix+"/"+serviceUIPath, WebIndexPage) + router.GET(pathPrefix+"/"+serviceUIPath+"/:basket", WebBasketPage) + //router.ServeFiles(pathPrefix+"/"+serviceUIPath+"/*filepath", http.Dir("./web")) // basket requests router.NotFound = http.HandlerFunc(AcceptBasketRequests) diff --git a/web_basket.html.go b/web_basket.html.go index a6836b8..60b5e34 100644 --- a/web_basket.html.go +++ b/web_basket.html.go @@ -4,7 +4,7 @@ const ( basketPageContentTemplate = ` - Request Basket: {{.}} + Request Basket: {{.Basket}} @@ -22,7 +22,7 @@ const (