diff --git a/README.md b/README.md index 34d36a8..b05ca4b 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ This default can be changed with labels: - `reproxy.ping` - ping path for the destination container. - `reproxy.remote` - restrict access to the route with a list of comma-separated subnets or ips - `reproxy.assets` - set assets mapping as `web-root:location`, for example `reproxy.assets=/web:/var/www` +- `reproxy.keep-host` - keep host header as is (`yes`, `true`, `1`) or replace with destination host (`no`, `false`, `0`) - `reproxy.enabled` - enable (`yes`, `true`, `1`) or disable (`no`, `false`, `0`) container from reproxy destinations. Pls note: without `--docker.auto` the destination container has to have at least one of `reproxy.*` labels to be considered as a potential destination. @@ -302,6 +303,7 @@ username1:bcrypt(password2) username2:bcrypt(password2) ... ``` +this can be generated with `htpasswd -nbB` command, i.e. `htpasswd -nbB test passwd` ## IP-based access control @@ -367,7 +369,8 @@ This is the list of all options supporting multiple elements: --basic-htpasswd= htpasswd file for basic auth [$BASIC_HTPASSWD] --lb-type=[random|failover|roundrobin] load balancer type (default: random) [$LB_TYPE] --signature enable reproxy signature headers [$SIGNATURE] - --remote-lookup-headers enable remote lookup headers [$REMOTE_LOOKUP_HEADERS] + --remote-lookup-headers enable remote lookup headers [$REMOTE_LOOKUP_HEADERS] + --keep-host keep original Host header as default when proxying [$KEEP_HOST] --insecure skip SSL verification on destination host [$INSECURE] --dbg debug mode [$DEBUG] diff --git a/app/discovery/discovery.go b/app/discovery/discovery.go index f1418ad..94b4543 100644 --- a/app/discovery/discovery.go +++ b/app/discovery/discovery.go @@ -37,6 +37,7 @@ type URLMapper struct { PingURL string MatchType MatchType RedirectType RedirectType + KeepHost *bool OnlyFromIPs []string AssetsLocation string // local FS root location @@ -427,15 +428,23 @@ func (s *Service) extendMapper(m URLMapper) URLMapper { return m } - m.Dst = strings.TrimSuffix(m.Dst, "/") + "/$1" - + res := URLMapper{ + Server: m.Server, + Dst: strings.TrimSuffix(m.Dst, "/") + "/$1", + ProviderID: m.ProviderID, + PingURL: m.PingURL, + MatchType: m.MatchType, + AssetsWebRoot: m.AssetsWebRoot, + AssetsLocation: m.AssetsLocation, + AssetsSPA: m.AssetsSPA, + } rx, err := regexp.Compile("^" + strings.TrimSuffix(src, "/") + "/(.*)") if err != nil { log.Printf("[WARN] can't extend %s, %v", m.SrcMatch.String(), err) return m } - m.SrcMatch = *rx - return m + res.SrcMatch = *rx + return res } // redirects process @code prefix and sets redirect type, i.e. "@302 /something" diff --git a/app/discovery/provider/consulcatalog/consulcatalog.go b/app/discovery/provider/consulcatalog/consulcatalog.go index 6beda13..a808d02 100644 --- a/app/discovery/provider/consulcatalog/consulcatalog.go +++ b/app/discovery/provider/consulcatalog/consulcatalog.go @@ -140,6 +140,7 @@ func (cc *ConsulCatalog) List() ([]discovery.URLMapper, error) { destURL := fmt.Sprintf("http://%s:%d/$1", c.ServiceAddress, c.ServicePort) pingURL := fmt.Sprintf("http://%s:%d/ping", c.ServiceAddress, c.ServicePort) server := "*" + var keepHost *bool onlyFrom := []string{} if v, ok := c.Labels["reproxy.enabled"]; ok && (v == "true" || v == "yes" || v == "1") { @@ -170,6 +171,19 @@ func (cc *ConsulCatalog) List() ([]discovery.URLMapper, error) { pingURL = fmt.Sprintf("http://%s:%d%s", c.ServiceAddress, c.ServicePort, v) } + if v, ok := c.Labels["reproxy.keep-host"]; ok { + enabled = true + if v == "true" || v == "yes" || v == "1" { + t := true + keepHost = &t + } else if v == "false" || v == "no" || v == "0" { + f := false + keepHost = &f + } else { + log.Printf("[WARN] invalid value for reproxy.keep-host: %s", v) + } + } + if !enabled { log.Printf("[DEBUG] service %s disabled", c.ServiceID) continue @@ -183,7 +197,7 @@ func (cc *ConsulCatalog) List() ([]discovery.URLMapper, error) { // server label may have multiple, comma separated servers for _, srv := range strings.Split(server, ",") { res = append(res, discovery.URLMapper{Server: strings.TrimSpace(srv), SrcMatch: *srcRegex, Dst: destURL, - OnlyFromIPs: onlyFrom, PingURL: pingURL, ProviderID: discovery.PIConsulCatalog}) + PingURL: pingURL, ProviderID: discovery.PIConsulCatalog, KeepHost: keepHost, OnlyFromIPs: onlyFrom}) } } diff --git a/app/discovery/provider/consulcatalog/consulcatalog_test.go b/app/discovery/provider/consulcatalog/consulcatalog_test.go index e5c973d..31a81ce 100644 --- a/app/discovery/provider/consulcatalog/consulcatalog_test.go +++ b/app/discovery/provider/consulcatalog/consulcatalog_test.go @@ -74,6 +74,20 @@ func TestConsulCatalog_List(t *testing.T) { ServicePort: 4000, Labels: map[string]string{"reproxy.enabled": "1"}, }, + { + ServiceID: "id5", + ServiceName: "name5", + ServiceAddress: "adr5", + ServicePort: 5000, + Labels: map[string]string{"reproxy.enabled": "true", "reproxy.keep-host": "true"}, + }, + { + ServiceID: "id6", + ServiceName: "name6", + ServiceAddress: "adr6", + ServicePort: 5001, + Labels: map[string]string{"reproxy.enabled": "true", "reproxy.keep-host": "false"}, + }, }, nil }} @@ -83,36 +97,54 @@ func TestConsulCatalog_List(t *testing.T) { res, err := cc.List() require.NoError(t, err) - require.Equal(t, 4, len(res)) + require.Equal(t, 6, len(res)) // sort slice for exclude random item positions after sorting by SrtMatch in List function sort.Slice(res, func(i, j int) bool { return len(res[i].Dst+res[i].Server) > len(res[j].Dst+res[j].Server) }) - assert.Equal(t, "^/api/123/(.*)", res[0].SrcMatch.String()) assert.Equal(t, "http://addr3:3000/blah/$1", res[0].Dst) assert.Equal(t, "example.com", res[0].Server) assert.Equal(t, "http://addr3:3000/ping", res[0].PingURL) + assert.Equal(t, (*bool)(nil), res[0].KeepHost) assert.Equal(t, []string{"127.0.0.1", "192.168.1.0/24"}, res[0].OnlyFromIPs) assert.Equal(t, "^/api/123/(.*)", res[1].SrcMatch.String()) assert.Equal(t, "http://addr3:3000/blah/$1", res[1].Dst) assert.Equal(t, "domain.com", res[1].Server) assert.Equal(t, "http://addr3:3000/ping", res[1].PingURL) + assert.Equal(t, (*bool)(nil), res[1].KeepHost) assert.Equal(t, []string{"127.0.0.1", "192.168.1.0/24"}, res[1].OnlyFromIPs) assert.Equal(t, "^/(.*)", res[2].SrcMatch.String()) assert.Equal(t, "http://addr44:4000/$1", res[2].Dst) assert.Equal(t, "http://addr44:4000/ping", res[2].PingURL) assert.Equal(t, "*", res[2].Server) + assert.Equal(t, (*bool)(nil), res[2].KeepHost) assert.Equal(t, []string{}, res[2].OnlyFromIPs) assert.Equal(t, "^/(.*)", res[3].SrcMatch.String()) assert.Equal(t, "http://addr2:2000/$1", res[3].Dst) assert.Equal(t, "http://addr2:2000/ping", res[3].PingURL) assert.Equal(t, "*", res[3].Server) + assert.Equal(t, (*bool)(nil), res[3].KeepHost) assert.Equal(t, []string{}, res[3].OnlyFromIPs) + + tr := true + assert.Equal(t, "^/(.*)", res[4].SrcMatch.String()) + assert.Equal(t, "http://adr5:5000/$1", res[4].Dst) + assert.Equal(t, "http://adr5:5000/ping", res[4].PingURL) + assert.Equal(t, "*", res[4].Server) + assert.Equal(t, &tr, res[4].KeepHost) + + fa := false + assert.Equal(t, "^/(.*)", res[5].SrcMatch.String()) + assert.Equal(t, "http://adr6:5001/$1", res[5].Dst) + assert.Equal(t, "http://adr6:5001/ping", res[5].PingURL) + assert.Equal(t, "*", res[5].Server) + assert.Equal(t, &fa, res[5].KeepHost) + } func TestConsulCatalog_serviceListWasChanged(t *testing.T) { diff --git a/app/discovery/provider/docker.go b/app/discovery/provider/docker.go index 6ced47d..70dfd45 100644 --- a/app/discovery/provider/docker.go +++ b/app/discovery/provider/docker.go @@ -163,6 +163,8 @@ func (d *Docker) parseContainerInfo(c containerInfo) (res []discovery.URLMapper) enabled = true } + keepHost := d.getKeepHostValue(c.Labels, n) + if !enabled { continue } @@ -176,7 +178,8 @@ func (d *Docker) parseContainerInfo(c containerInfo) (res []discovery.URLMapper) // docker server label may have multiple, comma separated servers for _, srv := range strings.Split(server, ",") { mp := discovery.URLMapper{Server: strings.TrimSpace(srv), SrcMatch: *srcRegex, Dst: destURL, - PingURL: pingURL, OnlyFromIPs: onlyFrom, ProviderID: discovery.PIDocker, MatchType: discovery.MTProxy} + PingURL: pingURL, ProviderID: discovery.PIDocker, MatchType: discovery.MTProxy, + KeepHost: keepHost, OnlyFromIPs: onlyFrom} // for assets we add the second proxy mapping only if explicitly requested if assetsWebRoot != "" && explicit { @@ -437,3 +440,23 @@ func (d *dockerClient) ListContainers() ([]containerInfo, error) { return containers, nil } + +func (d *Docker) getKeepHostValue(labels map[string]string, n int) *bool { + v, ok := d.labelN(labels, n, "keep-host") + if !ok { + return nil + } + + if v == "true" || v == "yes" || v == "y" || v == "1" { + k := true + return &k + } + + if v == "false" || v == "no" || v == "n" || v == "0" { + k := false + return &k + } + + log.Printf("[WARN] keep-host label value %s is not valid, ignoring", v) + return nil +} diff --git a/app/discovery/provider/docker_test.go b/app/discovery/provider/docker_test.go index 953a7b6..7efcf4f 100644 --- a/app/discovery/provider/docker_test.go +++ b/app/discovery/provider/docker_test.go @@ -137,6 +137,14 @@ func TestDocker_ListMulti(t *testing.T) { Name: "c5", State: "running", IP: "127.0.0.122", Ports: []int{2345}, // not enabled Labels: map[string]string{"reproxy.enabled": "false"}, }, + { + Name: "c6", State: "running", IP: "127.0.0.3", Ports: []int{12346}, + Labels: map[string]string{"reproxy.enabled": "y", "reproxy.keep-host": "y", "reproxy.route": "^/ky/"}, + }, + { + Name: "c7", State: "running", IP: "127.0.0.3", Ports: []int{12346}, + Labels: map[string]string{"reproxy.enabled": "y", "reproxy.keep-host": "n", "reproxy.route": "^/kn/"}, + }, }, nil }, } @@ -144,12 +152,13 @@ func TestDocker_ListMulti(t *testing.T) { d := Docker{DockerClient: dclient} res, err := d.List() require.NoError(t, err) - require.Equal(t, 6, len(res)) + require.Equal(t, 8, len(res)) assert.Equal(t, "^/api/123/(.*)", res[0].SrcMatch.String()) assert.Equal(t, "http://127.0.0.2:12345/blah/$1", res[0].Dst) assert.Equal(t, "example.com", res[0].Server) assert.Equal(t, "http://127.0.0.2:12345/ping", res[0].PingURL) + assert.Nil(t, res[0].KeepHost) assert.Equal(t, "/api/1/(.*)", res[1].SrcMatch.String()) assert.Equal(t, "http://127.0.0.3:7890/blah/1/$1", res[1].Dst) @@ -175,6 +184,12 @@ func TestDocker_ListMulti(t *testing.T) { assert.Equal(t, "http://127.0.0.2:12348/a/$1", res[5].Dst) assert.Equal(t, "http://127.0.0.2:12348/ping", res[5].PingURL) assert.Equal(t, "example.com", res[5].Server) + + assert.Equal(t, "^/ky/", res[6].SrcMatch.String()) + assert.Equal(t, true, *res[6].KeepHost) + + assert.Equal(t, "^/kn/", res[7].SrcMatch.String()) + assert.Equal(t, false, *res[7].KeepHost) } func TestDocker_ListMultiFallBack(t *testing.T) { diff --git a/app/discovery/provider/file.go b/app/discovery/provider/file.go index c78bed5..273bc28 100644 --- a/app/discovery/provider/file.go +++ b/app/discovery/provider/file.go @@ -84,6 +84,7 @@ func (d *File) List() (res []discovery.URLMapper, err error) { Ping string `yaml:"ping"` AssetsEnabled bool `yaml:"assets"` AssetsSPA bool `yaml:"spa"` + KeepHost *bool `yaml:"keep-host,omitempty"` OnlyFrom string `yaml:"remote"` } fh, err := os.Open(d.FileName) @@ -111,6 +112,7 @@ func (d *File) List() (res []discovery.URLMapper, err error) { SrcMatch: *rx, Dst: f.Dest, PingURL: f.Ping, + KeepHost: f.KeepHost, ProviderID: discovery.PIFile, MatchType: discovery.MTProxy, OnlyFromIPs: discovery.ParseOnlyFrom(f.OnlyFrom), diff --git a/app/discovery/provider/file_test.go b/app/discovery/provider/file_test.go index 4f1ac2c..bc870e6 100644 --- a/app/discovery/provider/file_test.go +++ b/app/discovery/provider/file_test.go @@ -113,6 +113,7 @@ func TestFile_List(t *testing.T) { assert.Equal(t, "", res[0].PingURL) assert.Equal(t, "srv.example.com", res[0].Server) assert.Equal(t, discovery.MTProxy, res[0].MatchType) + assert.Nil(t, res[0].KeepHost) assert.Equal(t, []string{}, res[0].OnlyFromIPs) assert.Equal(t, "^/api/svc1/(.*)", res[1].SrcMatch.String()) @@ -120,14 +121,16 @@ func TestFile_List(t *testing.T) { assert.Equal(t, "", res[1].PingURL) assert.Equal(t, "*", res[1].Server) assert.Equal(t, discovery.MTProxy, res[1].MatchType) - assert.Equal(t, []string{}, res[0].OnlyFromIPs) + assert.Nil(t, res[1].KeepHost) + assert.Equal(t, []string{}, res[1].OnlyFromIPs) assert.Equal(t, "/api/svc3/xyz", res[2].SrcMatch.String()) assert.Equal(t, "http://127.0.0.3:8080/blah3/xyz", res[2].Dst) assert.Equal(t, "http://127.0.0.3:8080/ping", res[2].PingURL) assert.Equal(t, "*", res[2].Server) assert.Equal(t, discovery.MTProxy, res[2].MatchType) - assert.Equal(t, []string{}, res[0].OnlyFromIPs) + assert.Nil(t, res[2].KeepHost) + assert.Equal(t, []string{}, res[2].OnlyFromIPs) assert.Equal(t, "/web/", res[3].SrcMatch.String()) assert.Equal(t, "/var/web", res[3].Dst) @@ -136,6 +139,7 @@ func TestFile_List(t *testing.T) { assert.Equal(t, discovery.MTStatic, res[3].MatchType) assert.Equal(t, false, res[3].AssetsSPA) assert.Equal(t, []string{"192.168.1.0/24", "124.0.0.1"}, res[3].OnlyFromIPs) + assert.Equal(t, true, *res[3].KeepHost) assert.Equal(t, "/web2/", res[4].SrcMatch.String()) assert.Equal(t, "/var/web2", res[4].Dst) @@ -143,5 +147,6 @@ func TestFile_List(t *testing.T) { assert.Equal(t, "*", res[4].Server) assert.Equal(t, discovery.MTStatic, res[4].MatchType) assert.Equal(t, true, res[4].AssetsSPA) - assert.Equal(t, []string{}, res[0].OnlyFromIPs) + assert.Equal(t, []string{}, res[4].OnlyFromIPs) + assert.Equal(t, false, *res[4].KeepHost) } diff --git a/app/discovery/provider/testdata/config.yml b/app/discovery/provider/testdata/config.yml index 85e618f..b277efd 100644 --- a/app/discovery/provider/testdata/config.yml +++ b/app/discovery/provider/testdata/config.yml @@ -1,7 +1,7 @@ default: - {route: "^/api/svc1/(.*)", dest: "http://127.0.0.1:8080/blah1/$1"} - {route: "/api/svc3/xyz", dest: "http://127.0.0.3:8080/blah3/xyz", "ping": "http://127.0.0.3:8080/ping"} - - {route: "/web/", dest: "/var/web", "assets": yes, "remote": "192.168.1.0/24, 124.0.0.1"} - - {route: "/web2/", dest: "/var/web2", "spa": yes} + - {route: "/web/", dest: "/var/web", "assets": yes, "keep-host": yes, "remote": "192.168.1.0/24, 124.0.0.1"} + - {route: "/web2/", dest: "/var/web2", "spa": yes, "keep-host": no} srv.example.com: - {route: "^/api/svc2/(.*)", dest: "http://127.0.0.2:8080/blah2/$1/abc"} diff --git a/app/main.go b/app/main.go index 0285267..c27d90d 100644 --- a/app/main.go +++ b/app/main.go @@ -37,6 +37,7 @@ var opts struct { RemoteLookupHeaders bool `long:"remote-lookup-headers" env:"REMOTE_LOOKUP_HEADERS" description:"enable remote lookup headers"` LBType string `long:"lb-type" env:"LB_TYPE" description:"load balancer type" choice:"random" choice:"failover" choice:"roundrobin" default:"random"` // nolint Insecure bool `long:"insecure" env:"INSECURE" description:"skip SSL certificate verification for the destination host"` + KeepHost bool `long:"keep-host" env:"KEEP_HOST" description:"pass the Host header from the client as-is, instead of rewriting it"` SSL struct { Type string `long:"type" env:"TYPE" description:"ssl (auto) support" choice:"none" choice:"static" choice:"auto" default:"none"` // nolint @@ -274,6 +275,7 @@ func run() error { ThrottleUser: opts.Throttle.User, BasicAuthEnabled: len(basicAuthAllowed) > 0, BasicAuthAllowed: basicAuthAllowed, + KeepHost: opts.KeepHost, OnlyFrom: makeOnlyFromMiddleware(), } diff --git a/app/proxy/proxy.go b/app/proxy/proxy.go index 0134a8c..32d4afe 100644 --- a/app/proxy/proxy.go +++ b/app/proxy/proxy.go @@ -55,6 +55,8 @@ type Http struct { // nolint golint ThrottleSystem int ThrottleUser int + + KeepHost bool } // Matcher source info (server and route) to the destination url @@ -199,6 +201,7 @@ const ( ctxURL = contextKey("url") ctxMatchType = contextKey("type") ctxMatch = contextKey("match") + ctxKeepHost = contextKey("keepHost") ) func (h *Http) proxyHandler() http.HandlerFunc { @@ -207,11 +210,15 @@ func (h *Http) proxyHandler() http.HandlerFunc { Director: func(r *http.Request) { ctx := r.Context() uu := ctx.Value(ctxURL).(*url.URL) + keepHost := ctx.Value(ctxKeepHost).(bool) r.Header.Add("X-Forwarded-Host", r.Host) r.URL.Path = uu.Path r.URL.Host = uu.Host r.URL.Scheme = uu.Scheme - r.Host = uu.Host + log.Printf("[DEBUG] keep host is %t", keepHost) + if !keepHost { + r.Host = uu.Host + } h.setXRealIP(r) }, Transport: &http.Transport{ @@ -325,6 +332,13 @@ func (h *Http) matchHandler(next http.Handler) http.Handler { return } ctx = context.WithValue(ctx, ctxURL, uu) // set destination url in request's context + var keepHost bool + if match.Mapper.KeepHost == nil { + keepHost = h.KeepHost + } else { + keepHost = *match.Mapper.KeepHost + } + ctx = context.WithValue(ctx, ctxKeepHost, keepHost) // set keep host in request's context } r = r.WithContext(ctx) }