mirror of
https://github.com/umputun/reproxy.git
synced 2025-02-16 18:34:30 +02:00
Implement Host header bypassing (#155)
* Initial implementation of keep-host argument * Add keep-host parsing to the consulcatalog provider * Update docs * update from the current master --------- by @ffix
This commit is contained in:
parent
7d4394f1c8
commit
fe24cf99ef
@ -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
|
||||
|
||||
@ -368,6 +370,7 @@ This is the list of all options supporting multiple elements:
|
||||
--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]
|
||||
--keep-host keep original Host header as default when proxying [$KEEP_HOST]
|
||||
--insecure skip SSL verification on destination host [$INSECURE]
|
||||
--dbg debug mode [$DEBUG]
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
}
|
||||
|
4
app/discovery/provider/testdata/config.yml
vendored
4
app/discovery/provider/testdata/config.yml
vendored
@ -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"}
|
||||
|
@ -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(),
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user