1
0
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:
Sergey Ninua 2024-01-25 12:28:54 +03:00 committed by GitHub
parent 7d4394f1c8
commit fe24cf99ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 135 additions and 16 deletions

View File

@ -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]

View File

@ -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"

View File

@ -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})
}
}

View File

@ -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) {

View File

@ -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
}

View File

@ -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) {

View File

@ -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),

View File

@ -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)
}

View File

@ -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"}

View File

@ -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(),
}

View File

@ -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)
}