From 6167400b28c900aa817b1bffc6fdddff50bb3eca Mon Sep 17 00:00:00 2001 From: funkiestj Date: Thu, 19 May 2022 16:07:59 -0700 Subject: [PATCH] api: add the ability to specify a path prefix (#12914) Specifically meant for when consul is behind a reverse proxy / API gateway Co-authored-by: Evan Culver --- .changelog/12914.txt | 3 +++ api/api.go | 19 ++++++++++++++- api/api_test.go | 56 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 .changelog/12914.txt diff --git a/.changelog/12914.txt b/.changelog/12914.txt new file mode 100644 index 0000000000..f19bf013ce --- /dev/null +++ b/.changelog/12914.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +api: add the ability to specify a path prefix for when consul is behind a reverse proxy or API gateway +``` diff --git a/api/api.go b/api/api.go index 8cc771c082..ff3be46070 100644 --- a/api/api.go +++ b/api/api.go @@ -320,6 +320,11 @@ type Config struct { // Scheme is the URI scheme for the Consul server Scheme string + // Prefix for URIs for when consul is behind an API gateway (reverse + // proxy). The API gateway must strip off the PathPrefix before + // passing the request onto consul. + PathPrefix string + // Datacenter to use. If not provided, the default agent datacenter is used. Datacenter string @@ -712,6 +717,18 @@ func NewClient(config *Config) (*Client, error) { return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0]) } config.Address = parts[1] + + // separate out a reverse proxy prefix, if it is present. + // NOTE: Rewriting this code to use url.Parse() instead of + // strings.SplitN() breaks existing test cases. + switch parts[0] { + case "http", "https": + parts := strings.SplitN(parts[1], "/", 2) + if len(parts) == 2 { + config.Address = parts[0] + config.PathPrefix = "/" + parts[1] + } + } } // If the TokenFile is set, always use that, even if a Token is configured. @@ -953,7 +970,7 @@ func (c *Client) newRequest(method, path string) *request { url: &url.URL{ Scheme: c.config.Scheme, Host: c.config.Address, - Path: path, + Path: c.config.PathPrefix + path, }, params: make(map[string][]string), header: c.Headers(), diff --git a/api/api_test.go b/api/api_test.go index f06f4c6823..a95a4044c5 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -1119,6 +1119,62 @@ func TestAPI_GenerateEnvHTTPS(t *testing.T) { require.Equal(t, expected, c.GenerateEnv()) } +// TestAPI_PrefixPath() validates that Config.Address is split into +// Config.Address and Config.PathPrefix as expected. If we want to add end to +// end testing in the future this will require configuring and running an +// API gateway / reverse proxy (e.g. nginx) +func TestAPI_PrefixPath(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + addr string + expectAddr string + expectPrefix string + }{ + { + name: "with http and prefix", + addr: "http://reverse.proxy.com/consul/path/prefix", + expectAddr: "reverse.proxy.com", + expectPrefix: "/consul/path/prefix", + }, + { + name: "with https and prefix", + addr: "https://reverse.proxy.com/consul/path/prefix", + expectAddr: "reverse.proxy.com", + expectPrefix: "/consul/path/prefix", + }, + { + name: "with http and no prefix", + addr: "http://localhost", + expectAddr: "localhost", + expectPrefix: "", + }, + { + name: "with https and no prefix", + addr: "https://localhost", + expectAddr: "localhost", + expectPrefix: "", + }, + { + name: "no scheme and no prefix", + addr: "localhost", + expectAddr: "localhost", + expectPrefix: "", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c := &Config{Address: tc.addr} + client, err := NewClient(c) + require.NoError(t, err) + require.Equal(t, tc.expectAddr, client.config.Address) + require.Equal(t, tc.expectPrefix, client.config.PathPrefix) + }) + } +} + func getExpectedCaPoolByDir(t *testing.T) *x509.CertPool { pool := x509.NewCertPool() entries, err := os.ReadDir("../test/ca_path")