From 7027f458dd95b2c4005f4565cc68e5d534a773cc Mon Sep 17 00:00:00 2001 From: Caleb Doxsey Date: Thu, 16 Apr 2020 09:12:12 -0600 Subject: [PATCH] config: add prefix, path and regex options proxy: support prefix, path and regex options --- config/policy.go | 5 ++++ proxy/proxy.go | 47 +++++++++++++++++++++++++++++++++++-- proxy/proxy_test.go | 56 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/config/policy.go b/config/policy.go index 833a5b362..d72912959 100644 --- a/config/policy.go +++ b/config/policy.go @@ -24,6 +24,11 @@ type Policy struct { Source *HostnameURL `yaml:",omitempty" json:"source,omitempty"` Destination *url.URL `yaml:",omitempty" json:"destination,omitempty"` + // Additional route matching options + Prefix string `mapstructure:"prefix" yaml:"prefix,omitempty" json:"prefix,omitempty"` + Path string `mapstructure:"path" yaml:"path,omitempty" json:"path,omitempty"` + Regex string `mapstructure:"regex" yaml:"regex,omitempty" json:"regex,omitempty"` + // Allow unauthenticated HTTP OPTIONS requests as per the CORS spec // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests CORSAllowPreflight bool `mapstructure:"cors_allow_preflight" yaml:"cors_allow_preflight,omitempty"` diff --git a/proxy/proxy.go b/proxy/proxy.go index 311f32f14..38f3f70c4 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -14,6 +14,8 @@ import ( "net/http" stdhttputil "net/http/httputil" "net/url" + "regexp" + "strings" "time" "github.com/gorilla/mux" @@ -230,8 +232,8 @@ func (p *Proxy) reverseProxyHandler(r *mux.Router, policy config.Policy) *mux.Ro // 4. Override any custom transport settings (e.g. TLS settings, etc) proxy.Transport = p.roundTripperFromPolicy(&policy) - // 5. Create a sub-router for a given route's hostname (`httpbin.corp.example.com`) - rp := r.Host(policy.Source.Host).Subrouter() + // 5. Create a sub-router with a matcher derived from the policy (host, path, etc...) + rp := r.MatcherFunc(routeMatcherFuncFromPolicy(policy)).Subrouter() rp.PathPrefix("/").Handler(proxy) // Optional: If websockets are enabled, do not set a handler request timeout @@ -323,3 +325,44 @@ func (p *Proxy) roundTripperFromPolicy(policy *config.Policy) http.RoundTripper func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.Handler.ServeHTTP(w, r) } + + +// routeMatcherFuncFromPolicy returns a mux matcher function which compares an http request with a policy. +// +// Routes can be filtered by the `source`, `prefix`, `path` and `regex` fields in the policy config. +func routeMatcherFuncFromPolicy(policy config.Policy) mux.MatcherFunc { + // match by source + sourceMatches := func(r *http.Request) bool { + return r.Host == policy.Source.Host && + strings.HasPrefix(r.URL.Path, policy.Source.Path) + } + + // match by prefix + prefixMatches := func(r *http.Request) bool { + return policy.Prefix == "" || + strings.HasPrefix(r.URL.Path, policy.Prefix) + } + + // match by path + pathMatches := func(r *http.Request) bool { + return policy.Path == "" || + r.URL.Path == policy.Path + } + + // match by path regex + var regexMatches func(*http.Request) bool + if policy.Regex == "" { + regexMatches = func(r *http.Request) bool { return true } + } else if re, err := regexp.Compile(policy.Regex); err == nil { + regexMatches = func(r *http.Request) bool { + return re.MatchString(r.URL.Path) + } + } else { + log.Error().Err(err).Str("regex", policy.Regex).Msg("proxy: invalid regex in policy, ignoring route") + regexMatches = func(r *http.Request) bool { return false } + } + + return func(r *http.Request, rm *mux.RouteMatch) bool { + return sourceMatches(r) && prefixMatches(r) && pathMatches(r) && regexMatches(r) + } +} \ No newline at end of file diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 47298a2c5..6f2a06609 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -276,3 +276,59 @@ func TestNewReverseProxy(t *testing.T) { t.Errorf("got body %q; expected %q", g, e) } } + +func TestRouteMatcherFuncFromPolicy(t *testing.T) { + tests := []struct { + source, prefix, path, regex string + incomingURL string + expect bool + msg string + }{ + // host in source + {"https://www.example.com", "", "", "", + "https://www.example.com", true, + "should match when host is the same as source host"}, + {"https://www.example.com", "", "", "", + "https://www.google.com", false, + "should not match when host is different from source host"}, + + // path prefix in source + {"https://www.example.com/admin", "", "", "", + "https://www.example.com/admin/someaction", true, + "should match when path begins with source path"}, + {"https://www.example.com/admin", "", "", "", + "https://www.example.com/notadmin", false, + "should not match when path does not begin with source path"}, + + // path prefix + {"https://www.example.com", "/admin", "", "", + "https://www.example.com/admin/someaction", true, + "should match when path begins with prefix"}, + {"https://www.example.com", "/admin", "", "", + "https://www.example.com/notadmin", false, + "should not match when path does not begin with prefix"}, + } + + for _, tt := range tests { + srcURL, err := url.Parse(tt.source) + if err != nil { + panic(err) + } + src := &config.HostnameURL{URL: srcURL} + matcher := routeMatcherFuncFromPolicy(config.Policy{ + Source: src, + Prefix: tt.prefix, + Path: tt.path, + Regex: tt.regex, + }) + req, err := http.NewRequest("GET", tt.incomingURL, nil) + if err != nil { + panic(err) + } + actual := matcher(req, nil) + if actual != tt.expect { + t.Errorf("%s (source=%s prefix=%s path=%s regex=%s incoming-url=%s)", + tt.msg, tt.source, tt.prefix, tt.path, tt.regex, tt.incomingURL) + } + } +}