config: add prefix, path and regex options

proxy: support prefix, path and regex options
This commit is contained in:
Caleb Doxsey 2020-04-16 09:12:12 -06:00 committed by Caleb Doxsey
parent 15972b9956
commit 7027f458dd
3 changed files with 106 additions and 2 deletions

View file

@ -24,6 +24,11 @@ type Policy struct {
Source *HostnameURL `yaml:",omitempty" json:"source,omitempty"` Source *HostnameURL `yaml:",omitempty" json:"source,omitempty"`
Destination *url.URL `yaml:",omitempty" json:"destination,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 // Allow unauthenticated HTTP OPTIONS requests as per the CORS spec
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests
CORSAllowPreflight bool `mapstructure:"cors_allow_preflight" yaml:"cors_allow_preflight,omitempty"` CORSAllowPreflight bool `mapstructure:"cors_allow_preflight" yaml:"cors_allow_preflight,omitempty"`

View file

@ -14,6 +14,8 @@ import (
"net/http" "net/http"
stdhttputil "net/http/httputil" stdhttputil "net/http/httputil"
"net/url" "net/url"
"regexp"
"strings"
"time" "time"
"github.com/gorilla/mux" "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) // 4. Override any custom transport settings (e.g. TLS settings, etc)
proxy.Transport = p.roundTripperFromPolicy(&policy) proxy.Transport = p.roundTripperFromPolicy(&policy)
// 5. Create a sub-router for a given route's hostname (`httpbin.corp.example.com`) // 5. Create a sub-router with a matcher derived from the policy (host, path, etc...)
rp := r.Host(policy.Source.Host).Subrouter() rp := r.MatcherFunc(routeMatcherFuncFromPolicy(policy)).Subrouter()
rp.PathPrefix("/").Handler(proxy) rp.PathPrefix("/").Handler(proxy)
// Optional: If websockets are enabled, do not set a handler request timeout // 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) { func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
p.Handler.ServeHTTP(w, r) 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)
}
}

View file

@ -276,3 +276,59 @@ func TestNewReverseProxy(t *testing.T) {
t.Errorf("got body %q; expected %q", g, e) 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)
}
}
}