diff --git a/core/admin.go b/core/admin.go index fa434f774..6126eb41d 100644 --- a/core/admin.go +++ b/core/admin.go @@ -99,7 +99,7 @@ func getAllHandlers(hoverfly *Hoverfly) []handlers.AdminHandler { &v2.HoverflyHandler{Hoverfly: hoverfly}, &v2.HoverflyDestinationHandler{Hoverfly: hoverfly}, &v2.HoverflyModeHandler{Hoverfly: hoverfly}, - &v2.HoverflyMiddlewareHandler{Hoverfly: hoverfly}, + &v2.HoverflyMiddlewareHandler{Hoverfly: hoverfly, Enabled: hoverfly.Cfg.EnableMiddlewareAPI}, &v2.HoverflyUsageHandler{Hoverfly: hoverfly}, &v2.HoverflyVersionHandler{Hoverfly: hoverfly}, &v2.HoverflyUpstreamProxyHandler{Hoverfly: hoverfly}, diff --git a/core/cmd/hoverfly/main.go b/core/cmd/hoverfly/main.go index b663282df..be9270ba3 100644 --- a/core/cmd/hoverfly/main.go +++ b/core/cmd/hoverfly/main.go @@ -121,6 +121,9 @@ var ( cors = flag.Bool("cors", false, "Enable CORS support") noImportCheck = flag.Bool("no-import-check", false, "Skip duplicate request check when importing simulations") + // Feature flags + enableMiddlewareAPI = flag.Bool("enable-middleware-api", false, "Enable the admin API to set middleware (PUT /api/v2/hoverfly/middleware)") + pacFile = flag.String("pac-file", "", "Path to the pac file to be imported on startup") clientAuthenticationDestination = flag.String("client-authentication-destination", "", "Regular expression of destination with client authentication") @@ -403,6 +406,9 @@ func main() { log.Info("Import check has been disabled") } + // Feature flags + cfg.EnableMiddlewareAPI = *enableMiddlewareAPI + cfg.ClientAuthenticationDestination = *clientAuthenticationDestination cfg.ClientAuthenticationClientCert = *clientAuthenticationClientCert cfg.ClientAuthenticationClientKey = *clientAuthenticationClientKey diff --git a/core/delay/log_normal_generator_test.go b/core/delay/log_normal_generator_test.go index 603b5a9d5..b86eb2b7c 100644 --- a/core/delay/log_normal_generator_test.go +++ b/core/delay/log_normal_generator_test.go @@ -1,14 +1,15 @@ package delay import ( + "sort" + "testing" + . "github.com/onsi/gomega" "gonum.org/v1/gonum/floats" "gonum.org/v1/gonum/stat" - "sort" - "testing" ) -const tolerance = 10 +const tolerance = 50 func TestLogNormalGenerator_GenerateDelay(t *testing.T) { RegisterTestingT(t) diff --git a/core/handlers/v2/hoverfly_middleware_handler.go b/core/handlers/v2/hoverfly_middleware_handler.go index 5281bd684..ff127a855 100644 --- a/core/handlers/v2/hoverfly_middleware_handler.go +++ b/core/handlers/v2/hoverfly_middleware_handler.go @@ -16,6 +16,7 @@ type HoverflyMiddleware interface { type HoverflyMiddlewareHandler struct { Hoverfly HoverflyMiddleware + Enabled bool } func (this *HoverflyMiddlewareHandler) RegisterRoutes(mux *bone.Mux, am *handlers.AuthHandler) { @@ -24,10 +25,12 @@ func (this *HoverflyMiddlewareHandler) RegisterRoutes(mux *bone.Mux, am *handler negroni.HandlerFunc(this.Get), )) - mux.Put("/api/v2/hoverfly/middleware", negroni.New( - negroni.HandlerFunc(am.RequireTokenAuthentication), - negroni.HandlerFunc(this.Put), - )) + if this.Enabled { + mux.Put("/api/v2/hoverfly/middleware", negroni.New( + negroni.HandlerFunc(am.RequireTokenAuthentication), + negroni.HandlerFunc(this.Put), + )) + } mux.Options("/api/v2/hoverfly/middleware", negroni.New( negroni.HandlerFunc(this.Options), )) @@ -60,6 +63,10 @@ func (this *HoverflyMiddlewareHandler) Put(w http.ResponseWriter, req *http.Requ } func (this *HoverflyMiddlewareHandler) Options(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - w.Header().Add("Allow", "OPTIONS, GET, PUT") + allow := "OPTIONS, GET" + if this.Enabled { + allow += ", PUT" + } + w.Header().Add("Allow", allow) handlers.WriteResponse(w, []byte("")) } diff --git a/core/handlers/v2/hoverfly_middleware_handler_test.go b/core/handlers/v2/hoverfly_middleware_handler_test.go index d182c688d..441e187bb 100644 --- a/core/handlers/v2/hoverfly_middleware_handler_test.go +++ b/core/handlers/v2/hoverfly_middleware_handler_test.go @@ -130,7 +130,7 @@ func Test_HoverflyMiddlewareHandler_Options_GetsOptions(t *testing.T) { RegisterTestingT(t) var stubHoverfly HoverflyMiddlewareStub - unit := HoverflyMiddlewareHandler{Hoverfly: &stubHoverfly} + unit := HoverflyMiddlewareHandler{Hoverfly: &stubHoverfly, Enabled: true} request, err := http.NewRequest("OPTIONS", "/api/v2/hoverfly/middleware", nil) Expect(err).To(BeNil()) diff --git a/core/settings.go b/core/settings.go index 88851ad06..835dab30a 100644 --- a/core/settings.go +++ b/core/settings.go @@ -1,11 +1,12 @@ package hoverfly import ( - "github.com/SpectoLabs/hoverfly/core/cors" "os" "strconv" "sync" + "github.com/SpectoLabs/hoverfly/core/cors" + "strings" "github.com/SpectoLabs/hoverfly/core/middleware" @@ -52,6 +53,9 @@ type Configuration struct { ResponsesBodyFilesPath string ResponsesBodyFilesAllowedOrigins []string + // Feature flags + EnableMiddlewareAPI bool + ProxyControlWG sync.WaitGroup mu sync.Mutex diff --git a/docs/pages/keyconcepts/middleware.rst b/docs/pages/keyconcepts/middleware.rst index e96678045..ed4afb768 100644 --- a/docs/pages/keyconcepts/middleware.rst +++ b/docs/pages/keyconcepts/middleware.rst @@ -25,13 +25,13 @@ You can write middleware in any language. There are two different types of middl Local Middleware ---------------- -Hoverfly has the ability to invoke middleware by executing a script or binary file on a host operating system. +Hoverfly has the ability to invoke middleware by executing a script or binary file on a host operating system. The only requires are that the provided middleware can be executed and sends the Middleware JSON schema to stdout when the Middleware JSON schema is received on stdin. HTTP Middleware --------------- -Hoverfly can also send middleware requests to a HTTP server instead of running a process locally. The benefits of this +Hoverfly can also send middleware requests to a HTTP server instead of running a process locally. The benefits of this are that Hoverfly does not initiate the process, giving more control to the user. The only requirements are that Hoverfly can POST the Middleware JSON schema to middleware URL provided and the middleware HTTP server responses with a 200 and the Middleware JSON schema is in the response. @@ -50,3 +50,30 @@ Hoverfly will send the JSON object to middleware via the standard input stream. .. seealso:: Middleware examples are covered in the tutorials section. See :ref:`randomlatency` and :ref:`modifyingresponses`. + + +Security and availability of the Set Middleware API +--------------------------------------------------- + +By default, the admin endpoint to set middleware (PUT /api/v2/hoverfly/middleware) is disabled. To enable it: + +- When starting Hoverfly directly: run with the flag: -enable-middleware-api +- When starting via hoverctl: use the same flag on start: hoverctl start --enable-middleware-api + +Network binding and remote access +--------------------------------- + +By default, Hoverfly binds its Admin and Proxy ports to the loopback interface only (127.0.0.1). This means the Admin API is not reachable from remote hosts out of the box. + +.. warning:: + + Exposing the Admin API outside localhost increases risk, especially if the Set Middleware API is enabled, because it allows executing arbitrary scripts/binaries on the host (for local middleware) or invoking remote middleware services. + + If you expose the Admin API and enable the Set Middleware API, you should: + + - Run Hoverfly only on trusted/private networks. + - Restrict access to the Admin API to trusted callers and networks (e.g., via firewalls, security groups, VPNs, reverse proxy ACLs). + - Prefer binding to localhost unless there is a strong need to expose it, and scope exposure to the minimum required interfaces. + - :ref:`proxyauth` if appropriate and avoid exposing the Admin port publicly. + +The guidance above applies whether you configure middleware as a local executable/script or as HTTP middleware. diff --git a/functional-tests/core/api/middleware_api_v2_test.go b/functional-tests/core/api/middleware_api_v2_test.go index 47f132407..2fb2069ce 100644 --- a/functional-tests/core/api/middleware_api_v2_test.go +++ b/functional-tests/core/api/middleware_api_v2_test.go @@ -18,7 +18,7 @@ var _ = Describe("/api/v2/hoverfly/middleware", func() { BeforeEach(func() { hoverfly = functional_tests.NewHoverfly() - hoverfly.Start() + hoverfly.Start("-enable-middleware-api") }) AfterEach(func() { @@ -56,3 +56,30 @@ var _ = Describe("/api/v2/hoverfly/middleware", func() { }) }) }) + + +var _ = Describe("/api/v2/hoverfly/middleware when disabled", func() { + + var ( + hoverfly *functional_tests.Hoverfly + ) + + BeforeEach(func() { + hoverfly = functional_tests.NewHoverfly() + hoverfly.Start() + }) + + AfterEach(func() { + hoverfly.Stop() + }) + + Context("PUT", func() { + + It("Should return 404 when setting middleware", func() { + req := sling.New().Put("http://localhost:" + hoverfly.GetAdminPort() + "/api/v2/hoverfly/middleware") + req.Body(strings.NewReader(`{"binary":"ruby","script":"puts 'hi'"}`)) + res := functional_tests.DoRequest(req) + Expect(res.StatusCode).To(Equal(404)) + }) + }) +}) diff --git a/functional-tests/core/ft_simulate_mode_test.go b/functional-tests/core/ft_simulate_mode_test.go index 1281f6e66..23769a26a 100644 --- a/functional-tests/core/ft_simulate_mode_test.go +++ b/functional-tests/core/ft_simulate_mode_test.go @@ -21,7 +21,7 @@ var _ = Describe("When I run Hoverfly in simulate mode", func() { BeforeEach(func() { hoverfly = functional_tests.NewHoverfly() - hoverfly.Start() + hoverfly.Start("-enable-middleware-api") hoverfly.SetMode("simulate") }) diff --git a/functional-tests/hoverctl/middleware_test.go b/functional-tests/hoverctl/middleware_test.go index c2e5b3e4d..a6ed694ab 100644 --- a/functional-tests/hoverctl/middleware_test.go +++ b/functional-tests/hoverctl/middleware_test.go @@ -23,7 +23,7 @@ var _ = Describe("When I use hoverctl", func() { BeforeEach(func() { hoverfly = functional_tests.NewHoverfly() - hoverfly.Start() + hoverfly.Start("-enable-middleware-api") hoverfly.SetMiddleware("ruby", "#!/usr/bin/env ruby\n# encoding: utf-8\nwhile payload = STDIN.gets\nnext unless payload\n\nSTDOUT.puts payload\nend") functional_tests.Run(hoverctlBinary, "targets", "update", "local", "--admin-port", hoverfly.GetAdminPort()) diff --git a/functional-tests/hoverctl/start_test.go b/functional-tests/hoverctl/start_test.go index 4cbbda920..aaa91a4e9 100644 --- a/functional-tests/hoverctl/start_test.go +++ b/functional-tests/hoverctl/start_test.go @@ -1,9 +1,12 @@ package hoverctl_suite import ( - "github.com/antonholmquist/jason" + "io" "io/ioutil" "strconv" + "strings" + + "github.com/antonholmquist/jason" "github.com/SpectoLabs/hoverfly/core/authentication/backends" "github.com/SpectoLabs/hoverfly/functional-tests" @@ -327,7 +330,21 @@ var _ = Describe("hoverctl `start`", func() { }) }) - Context("with pac-file", func() { + Context("with enable-middleware-api", func() { + It("enables middleware API when flag is provided", func() { + output := functional_tests.Run(hoverctlBinary, "start", "--enable-middleware-api") + Expect(output).To(ContainSubstring("Hoverfly is now running")) + + // Attempt to set middleware via Admin API should be allowed (200) + req := sling.New().Put("http://localhost:8888/api/v2/hoverfly/middleware") + // Use a valid echo middleware that reads from STDIN and outputs the JSON payload unchanged + req.Body(strings.NewReader(`{"binary":"ruby", "script":"#!/usr/bin/env ruby\n# encoding: utf-8\nwhile payload = STDIN.gets\nnext unless payload\n\nSTDOUT.puts payload\nend"}`)) + res := functional_tests.DoRequest(req) + Expect(res.StatusCode).To(Equal(200)) + }) + }) + + Context("with pac-file", func() { BeforeEach(func() { }) @@ -338,7 +355,7 @@ var _ = Describe("hoverctl `start`", func() { response := functional_tests.DoRequest(sling.New().Get("http://localhost:8888/api/v2/hoverfly/pac")) Expect(response.StatusCode).To(Equal(200)) - responseBody, err := ioutil.ReadAll(response.Body) + responseBody, err := io.ReadAll(response.Body) Expect(err).To(BeNil()) Expect(string(responseBody)).To(ContainSubstring(`function FindProxyForURL(url, host) {`)) }) diff --git a/functional-tests/hoverctl/status_test.go b/functional-tests/hoverctl/status_test.go index ff155b316..74aa5d045 100644 --- a/functional-tests/hoverctl/status_test.go +++ b/functional-tests/hoverctl/status_test.go @@ -16,7 +16,7 @@ var _ = Describe("when I use hoverctl status", func() { BeforeEach(func() { hoverfly = functional_tests.NewHoverfly() - hoverfly.Start() + hoverfly.Start("-enable-middleware-api") functional_tests.Run(hoverctlBinary, "targets", "update", "local", "--admin-port", hoverfly.GetAdminPort()) }) diff --git a/hoverctl/cmd/start.go b/hoverctl/cmd/start.go index 1cb061052..8b138211b 100644 --- a/hoverctl/cmd/start.go +++ b/hoverctl/cmd/start.go @@ -78,6 +78,9 @@ hoverctl configuration file. target.Simulations, _ = cmd.Flags().GetStringSlice("import") + // Feature flags + target.EnableMiddlewareAPI, _ = cmd.Flags().GetBool("enable-middleware-api") + if pacFileLocation, _ := cmd.Flags().GetString("pac-file"); pacFileLocation != "" { pacFileData, err := configuration.ReadFile(pacFileLocation) @@ -199,4 +202,7 @@ func init() { startCmd.Flags().StringSlice("logs-output", []string{}, "Locations for log output, \"console\"(default) or \"file\"") startCmd.Flags().String("logs-file", "", "Log file name. Use \"hoverfly-.log\" if not provided") startCmd.Flags().String("log-level", "info", "Set log level (panic, fatal, error, warn, info or debug)") + + // Feature flags + startCmd.Flags().Bool("enable-middleware-api", false, "Enable the admin API to set middleware (PUT /api/v2/hoverfly/middleware)") } diff --git a/hoverctl/configuration/target.go b/hoverctl/configuration/target.go index 4a39bece1..0bd251259 100644 --- a/hoverctl/configuration/target.go +++ b/hoverctl/configuration/target.go @@ -33,6 +33,9 @@ type Target struct { ClientAuthenticationClientKey string `yaml:",omitempty"` ClientAuthenticationCACert string `yaml:",omitempty"` + // Feature flags + EnableMiddlewareAPI bool `yaml:",omitempty"` + AuthEnabled bool Username string Password string @@ -171,5 +174,10 @@ func (this Target) BuildFlags() Flags { } } + // Feature flags + if this.EnableMiddlewareAPI { + flags = append(flags, "-enable-middleware-api") + } + return flags }