Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
6 changes: 6 additions & 0 deletions core/cmd/hoverfly/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions core/delay/log_normal_generator_test.go
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
17 changes: 12 additions & 5 deletions core/handlers/v2/hoverfly_middleware_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type HoverflyMiddleware interface {

type HoverflyMiddlewareHandler struct {
Hoverfly HoverflyMiddleware
Enabled bool
}

func (this *HoverflyMiddlewareHandler) RegisterRoutes(mux *bone.Mux, am *handlers.AuthHandler) {
Expand All @@ -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),
))
Expand Down Expand Up @@ -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(""))
}
2 changes: 1 addition & 1 deletion core/handlers/v2/hoverfly_middleware_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
6 changes: 5 additions & 1 deletion core/settings.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -52,6 +53,9 @@ type Configuration struct {
ResponsesBodyFilesPath string
ResponsesBodyFilesAllowedOrigins []string

// Feature flags
EnableMiddlewareAPI bool

ProxyControlWG sync.WaitGroup

mu sync.Mutex
Expand Down
31 changes: 29 additions & 2 deletions docs/pages/keyconcepts/middleware.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
29 changes: 28 additions & 1 deletion functional-tests/core/api/middleware_api_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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))
})
})
})
2 changes: 1 addition & 1 deletion functional-tests/core/ft_simulate_mode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})

Expand Down
2 changes: 1 addition & 1 deletion functional-tests/hoverctl/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
23 changes: 20 additions & 3 deletions functional-tests/hoverctl/start_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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() {
})

Expand All @@ -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) {`))
})
Expand Down
2 changes: 1 addition & 1 deletion functional-tests/hoverctl/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
Expand Down
6 changes: 6 additions & 0 deletions hoverctl/cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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-<target name>.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)")
}
8 changes: 8 additions & 0 deletions hoverctl/configuration/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -171,5 +174,10 @@ func (this Target) BuildFlags() Flags {
}
}

// Feature flags
if this.EnableMiddlewareAPI {
flags = append(flags, "-enable-middleware-api")
}

return flags
}
Loading