diff --git a/config/config.go b/config/config.go index fc6486e30..7a2efd3bb 100644 --- a/config/config.go +++ b/config/config.go @@ -7,16 +7,17 @@ import ( ) type Config struct { - Proxy Proxy - Registry Registry - Listen []Listen - Log Log - Metrics Metrics - UI UI - Runtime Runtime - ProfileMode string - ProfilePath string - Insecure bool + Proxy Proxy + Registry Registry + Listen []Listen + Log Log + Metrics Metrics + UI UI + Runtime Runtime + ProfileMode string + ProfilePath string + Insecure bool + DisableGlobMatching bool } type CertSource struct { diff --git a/config/load.go b/config/load.go index 098d0c2c1..3db6fb3d4 100644 --- a/config/load.go +++ b/config/load.go @@ -187,6 +187,7 @@ func load(cmdline, environ, envprefix []string, props *properties.Properties) (c f.StringVar(&cfg.UI.Title, "ui.title", defaultConfig.UI.Title, "optional title for the UI") f.StringVar(&cfg.ProfileMode, "profile.mode", defaultConfig.ProfileMode, "enable profiling mode, one of [cpu, mem, mutex, block]") f.StringVar(&cfg.ProfilePath, "profile.path", defaultConfig.ProfilePath, "path to profile dump file") + f.BoolVar(&cfg.DisableGlobMatching, "glob.matching.disabled", defaultConfig.DisableGlobMatching, "Disable Glob Matching on routes, one of [true, false]") // deprecated flags var proxyLogRoutes string diff --git a/docs/content/ref/glob.matching.disabled.md b/docs/content/ref/glob.matching.disabled.md new file mode 100644 index 000000000..ec1e41e00 --- /dev/null +++ b/docs/content/ref/glob.matching.disabled.md @@ -0,0 +1,11 @@ +--- +title: "glob.matching.disabled" +--- + +`glob.matching.disabled` disables glob matching on route lookups. + +Valid options are `true`, `false` + +The default is + + glob.matching.disabled = false diff --git a/fabio.properties b/fabio.properties index 703027d3d..e723d454f 100644 --- a/fabio.properties +++ b/fabio.properties @@ -749,6 +749,17 @@ # registry.consul.checksRequired = one +# glob.matching.disabled disables glob matching on route lookups +# If glob matching is enabled there is a performance decrease +# for every route lookup. At a large number of services (> 500) this +# can have a significant impact on performance. If glob matching is disabled +# Fabio performs a static string compare for route lookups. +# +# The default is +# +# glob.matching.disabled = false + + # metrics.target configures the backend the metrics values are # sent to. # diff --git a/main.go b/main.go index 264c30978..3ccf43b97 100644 --- a/main.go +++ b/main.go @@ -183,7 +183,7 @@ func newHTTPProxy(cfg *config.Config) http.Handler { Transport: newTransport(nil), InsecureTransport: newTransport(&tls.Config{InsecureSkipVerify: true}), Lookup: func(r *http.Request) *route.Target { - t := route.GetTable().Lookup(r, r.Header.Get("trace"), pick, match) + t := route.GetTable().Lookup(r, r.Header.Get("trace"), pick, match, cfg.DisableGlobMatching) if t == nil { notFound.Inc(1) log.Print("[WARN] No route for ", r.Host, r.URL) diff --git a/proxy/http_integration_test.go b/proxy/http_integration_test.go index ddc6c4c4c..2132d3dd9 100644 --- a/proxy/http_integration_test.go +++ b/proxy/http_integration_test.go @@ -27,6 +27,12 @@ import ( "github.com/pascaldekloe/goe/verify" ) +const ( + // helper constants for the Lookup function + globEnabled = false + globDisabled = true +) + func TestProxyProducesCorrectXForwardedSomethingHeader(t *testing.T) { var hdr http.Header = make(http.Header) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -188,7 +194,7 @@ func TestProxyStripsPath(t *testing.T) { Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable("route add mock /foo/bar " + server.URL + ` opts "strip=/foo"`) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) }, }) defer proxy.Close() @@ -224,7 +230,7 @@ func TestProxyHost(t *testing.T) { }, }, Lookup: func(r *http.Request) *route.Target { - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) }, }) defer proxy.Close() @@ -271,7 +277,7 @@ func TestHostRedirect(t *testing.T) { Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { r.Host = "c.com" - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) }, }) defer proxy.Close() @@ -310,7 +316,7 @@ func TestPathRedirect(t *testing.T) { proxy := httptest.NewServer(&HTTPProxy{ Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) }, }) defer proxy.Close() @@ -478,7 +484,7 @@ func TestProxyHTTPSUpstream(t *testing.T) { Transport: &http.Transport{TLSClientConfig: tlsClientConfig()}, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable("route add srv / " + server.URL + ` opts "proto=https"`) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) }, }) defer proxy.Close() @@ -497,7 +503,6 @@ func TestProxyHTTPSUpstreamSkipVerify(t *testing.T) { server.TLS = &tls.Config{} server.StartTLS() defer server.Close() - proxy := httptest.NewServer(&HTTPProxy{ Config: config.Proxy{}, Transport: http.DefaultTransport, @@ -506,7 +511,7 @@ func TestProxyHTTPSUpstreamSkipVerify(t *testing.T) { }, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable("route add srv / " + server.URL + ` opts "proto=https tlsskipverify=true"`) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) }, }) defer proxy.Close() @@ -707,7 +712,7 @@ func BenchmarkProxyLogger(b *testing.B) { Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable("route add mock / " + server.URL) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globEnabled) }, Logger: l, } diff --git a/proxy/listen_test.go b/proxy/listen_test.go index 607461641..c795e51b2 100644 --- a/proxy/listen_test.go +++ b/proxy/listen_test.go @@ -22,6 +22,8 @@ func TestGracefulShutdown(t *testing.T) { })) defer srv.Close() + globDisabled := false + // start proxy addr := "127.0.0.1:57777" var wg sync.WaitGroup @@ -32,7 +34,7 @@ func TestGracefulShutdown(t *testing.T) { Transport: http.DefaultTransport, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable("route add svc / " + srv.URL) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globDisabled) }, } l := config.Listen{Addr: addr} diff --git a/proxy/ws_integration_test.go b/proxy/ws_integration_test.go index a03977481..853371ca2 100644 --- a/proxy/ws_integration_test.go +++ b/proxy/ws_integration_test.go @@ -33,6 +33,8 @@ func TestProxyWSUpstream(t *testing.T) { defer wssServer.Close() t.Log("Started WSS server: ", wssServer.URL) + globDisabled := false + routes := "route add ws /ws " + wsServer.URL + "\n" routes += "route add ws /wss " + wssServer.URL + ` opts "proto=https"` + "\n" routes += "route add ws /insecure " + wssServer.URL + ` opts "proto=https tlsskipverify=true"` + "\n" @@ -44,7 +46,7 @@ func TestProxyWSUpstream(t *testing.T) { InsecureTransport: &http.Transport{TLSClientConfig: tlsInsecureConfig()}, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(routes) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globDisabled) }, }) defer httpProxy.Close() @@ -56,7 +58,7 @@ func TestProxyWSUpstream(t *testing.T) { InsecureTransport: &http.Transport{TLSClientConfig: tlsInsecureConfig()}, Lookup: func(r *http.Request) *route.Target { tbl, _ := route.NewTable(routes) - return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"], globDisabled) }, }) httpsProxy.TLS = tlsServerConfig() diff --git a/route/issue57_test.go b/route/issue57_test.go index a0f8fb2cc..c87dd8b00 100644 --- a/route/issue57_test.go +++ b/route/issue57_test.go @@ -32,7 +32,7 @@ func TestIssue57(t *testing.T) { if err != nil { t.Fatalf("%d: got %v want nil", i, err) } - target := tbl.Lookup(req, "", rrPicker, prefixMatcher) + target := tbl.Lookup(req, "", rrPicker, prefixMatcher, globEnabled) if target == nil { t.Fatalf("%d: got %v want %v", i, target, want) } diff --git a/route/route_bench_test.go b/route/route_bench_test.go index 2f9126710..e5693c572 100644 --- a/route/route_bench_test.go +++ b/route/route_bench_test.go @@ -123,8 +123,9 @@ func makeRequests(t Table) []*http.Request { func benchmarkGet(t Table, match matcher, pick picker, pb *testing.PB) { reqs := makeRequests(t) k, n := len(reqs), 0 + //Glob Matching True for pb.Next() { - t.Lookup(reqs[n%k], "", pick, match) + t.Lookup(reqs[n%k], "", pick, match, globEnabled) n++ } } diff --git a/route/table.go b/route/table.go index 117543e77..4e12fc182 100644 --- a/route/table.go +++ b/route/table.go @@ -299,6 +299,8 @@ func (t Table) matchingHosts(req *http.Request) (hosts []string) { host := normalizeHost(req.Host, req.TLS != nil) for pattern := range t { normpat := normalizeHost(pattern, req.TLS != nil) + // TODO setup compiled GLOBs in a separate MAP + // TODO Issue #548 g := glob.MustCompile(normpat) if g.Match(host) { hosts = append(hosts, pattern) @@ -327,6 +329,24 @@ func (t Table) matchingHosts(req *http.Request) (hosts []string) { return } +// Issue 548 - Added separate func +// +// matchingHostNoGlob returns the route from the +// routing table which matches the normalized request hostname. +func (t Table) matchingHostNoGlob(req *http.Request) (hosts []string) { + host := normalizeHost(req.Host, req.TLS != nil) + + for pattern := range t { + normpat := normalizeHost(pattern, req.TLS != nil) + if normpat == host { + //log.Printf("DEBUG Matched %s and %s", normpat, host) + hosts = append(hosts, pattern) + return + } + } + return +} + // Reverse returns its argument string reversed rune-wise left to right. // // taken from https://github.com/golang/example/blob/master/stringutil/reverse.go @@ -342,7 +362,9 @@ func Reverse(s string) string { // or nil if there is none. It first checks the routes for the host // and if none matches then it falls back to generic routes without // a host. This is useful for a catch-all '/' rule. -func (t Table) Lookup(req *http.Request, trace string, pick picker, match matcher) (target *Target) { +func (t Table) Lookup(req *http.Request, trace string, pick picker, match matcher, globDisabled bool) (target *Target) { + + var hosts []string if trace != "" { if len(trace) > 16 { trace = trace[:15] @@ -352,7 +374,14 @@ func (t Table) Lookup(req *http.Request, trace string, pick picker, match matche // find matching hosts for the request // and add "no host" as the fallback option - hosts := t.matchingHosts(req) + // if globDisabled then match without Glob + // Issue 548 + if globDisabled { + hosts = t.matchingHostNoGlob(req) + } else { + hosts = t.matchingHosts(req) + } + if trace != "" { log.Printf("[TRACE] %s Matching hosts: %v", trace, hosts) } diff --git a/route/table_test.go b/route/table_test.go index 808c9168b..824693c48 100644 --- a/route/table_test.go +++ b/route/table_test.go @@ -11,6 +11,12 @@ import ( "testing" ) +const ( + // helper constants for the Lookup function + globEnabled = false + globDisabled = true +) + func TestTableParse(t *testing.T) { genRoutes := func(n int, format string) (a []string) { for i := 0; i < n; i++ { @@ -499,8 +505,9 @@ func TestTableLookupIssue448(t *testing.T) { } var tests = []struct { - req *http.Request - dst string + req *http.Request + dst string + globEnabled bool }{ { req: &http.Request{ @@ -551,7 +558,7 @@ func TestTableLookupIssue448(t *testing.T) { } for i, tt := range tests { - if got, want := tbl.Lookup(tt.req, "", rndPicker, prefixMatcher).URL.String(), tt.dst; got != want { + if got, want := tbl.Lookup(tt.req, "", rndPicker, prefixMatcher, globEnabled).URL.String(), tt.dst; got != want { t.Errorf("%d: got %v want %v", i, got, want) } } @@ -580,53 +587,54 @@ func TestTableLookup(t *testing.T) { } var tests = []struct { - req *http.Request - dst string + req *http.Request + dst string + globEnabled bool }{ // match on host and path with and without trailing slash - {&http.Request{Host: "abc.com", URL: mustParse("/")}, "http://foo.com:1000"}, - {&http.Request{Host: "abc.com", URL: mustParse("/bar")}, "http://foo.com:1000"}, - {&http.Request{Host: "abc.com", URL: mustParse("/foo")}, "http://foo.com:1500"}, - {&http.Request{Host: "abc.com", URL: mustParse("/foo/")}, "http://foo.com:2000"}, - {&http.Request{Host: "abc.com", URL: mustParse("/foo/bar")}, "http://foo.com:2500"}, - {&http.Request{Host: "abc.com", URL: mustParse("/foo/bar/")}, "http://foo.com:3000"}, + {&http.Request{Host: "abc.com", URL: mustParse("/")}, "http://foo.com:1000", globEnabled}, + {&http.Request{Host: "abc.com", URL: mustParse("/bar")}, "http://foo.com:1000", globEnabled}, + {&http.Request{Host: "abc.com", URL: mustParse("/foo")}, "http://foo.com:1500", globEnabled}, + {&http.Request{Host: "abc.com", URL: mustParse("/foo/")}, "http://foo.com:2000", globEnabled}, + {&http.Request{Host: "abc.com", URL: mustParse("/foo/bar")}, "http://foo.com:2500", globEnabled}, + {&http.Request{Host: "abc.com", URL: mustParse("/foo/bar/")}, "http://foo.com:3000", globEnabled}, // do not match on host but maybe on path - {&http.Request{Host: "def.com", URL: mustParse("/")}, "http://foo.com:800"}, - {&http.Request{Host: "def.com", URL: mustParse("/bar")}, "http://foo.com:800"}, - {&http.Request{Host: "def.com", URL: mustParse("/foo")}, "http://foo.com:900"}, + {&http.Request{Host: "def.com", URL: mustParse("/")}, "http://foo.com:800", globEnabled}, + {&http.Request{Host: "def.com", URL: mustParse("/bar")}, "http://foo.com:800", globEnabled}, + {&http.Request{Host: "def.com", URL: mustParse("/foo")}, "http://foo.com:900", globEnabled}, // strip default port - {&http.Request{Host: "abc.com:80", URL: mustParse("/")}, "http://foo.com:1000"}, - {&http.Request{Host: "abc.com:443", URL: mustParse("/"), TLS: &tls.ConnectionState{}}, "http://foo.com:1000"}, + {&http.Request{Host: "abc.com:80", URL: mustParse("/")}, "http://foo.com:1000", globEnabled}, + {&http.Request{Host: "abc.com:443", URL: mustParse("/"), TLS: &tls.ConnectionState{}}, "http://foo.com:1000", globEnabled}, // not using default port - {&http.Request{Host: "abc.com:443", URL: mustParse("/")}, "http://foo.com:800"}, - {&http.Request{Host: "abc.com:80", URL: mustParse("/"), TLS: &tls.ConnectionState{}}, "http://foo.com:800"}, + {&http.Request{Host: "abc.com:443", URL: mustParse("/")}, "http://foo.com:800", globEnabled}, + {&http.Request{Host: "abc.com:80", URL: mustParse("/"), TLS: &tls.ConnectionState{}}, "http://foo.com:800", globEnabled}, // glob match the host - {&http.Request{Host: "x.abc.com", URL: mustParse("/")}, "http://foo.com:4000"}, - {&http.Request{Host: "y.abc.com", URL: mustParse("/abc")}, "http://foo.com:4000"}, - {&http.Request{Host: "x.abc.com", URL: mustParse("/foo/")}, "http://foo.com:5000"}, - {&http.Request{Host: "y.abc.com", URL: mustParse("/foo/")}, "http://foo.com:5000"}, - {&http.Request{Host: ".abc.com", URL: mustParse("/foo/")}, "http://foo.com:5000"}, - {&http.Request{Host: "x.y.abc.com", URL: mustParse("/foo/")}, "http://foo.com:5000"}, - {&http.Request{Host: "y.abc.com:80", URL: mustParse("/foo/")}, "http://foo.com:5000"}, - {&http.Request{Host: "x.aaa.abc.com", URL: mustParse("/")}, "http://foo.com:6000"}, - {&http.Request{Host: "x.aaa.abc.com", URL: mustParse("/foo")}, "http://foo.com:6000"}, - {&http.Request{Host: "x.bbb.abc.com", URL: mustParse("/")}, "http://foo.com:6100"}, - {&http.Request{Host: "x.bbb.abc.com", URL: mustParse("/foo")}, "http://foo.com:6100"}, - {&http.Request{Host: "y.abc.com:443", URL: mustParse("/foo/"), TLS: &tls.ConnectionState{}}, "http://foo.com:5000"}, + {&http.Request{Host: "x.abc.com", URL: mustParse("/")}, "http://foo.com:4000", globEnabled}, + {&http.Request{Host: "y.abc.com", URL: mustParse("/abc")}, "http://foo.com:4000", globEnabled}, + {&http.Request{Host: "x.abc.com", URL: mustParse("/foo/")}, "http://foo.com:5000", globEnabled}, + {&http.Request{Host: "y.abc.com", URL: mustParse("/foo/")}, "http://foo.com:5000", globEnabled}, + {&http.Request{Host: ".abc.com", URL: mustParse("/foo/")}, "http://foo.com:5000", globEnabled}, + {&http.Request{Host: "x.y.abc.com", URL: mustParse("/foo/")}, "http://foo.com:5000", globEnabled}, + {&http.Request{Host: "y.abc.com:80", URL: mustParse("/foo/")}, "http://foo.com:5000", globEnabled}, + {&http.Request{Host: "x.aaa.abc.com", URL: mustParse("/")}, "http://foo.com:6000", globEnabled}, + {&http.Request{Host: "x.aaa.abc.com", URL: mustParse("/foo")}, "http://foo.com:6000", globEnabled}, + {&http.Request{Host: "x.bbb.abc.com", URL: mustParse("/")}, "http://foo.com:6100", globEnabled}, + {&http.Request{Host: "x.bbb.abc.com", URL: mustParse("/foo")}, "http://foo.com:6100", globEnabled}, + {&http.Request{Host: "y.abc.com:443", URL: mustParse("/foo/"), TLS: &tls.ConnectionState{}}, "http://foo.com:5000", globEnabled}, // exact match has precedence over glob match - {&http.Request{Host: "z.abc.com", URL: mustParse("/foo/")}, "http://foo.com:3100"}, + {&http.Request{Host: "z.abc.com", URL: mustParse("/foo/")}, "http://foo.com:3100", globEnabled}, // explicit port on route - {&http.Request{Host: "xyz.com", URL: mustParse("/")}, "https://xyz.com"}, + {&http.Request{Host: "xyz.com", URL: mustParse("/")}, "https://xyz.com", globEnabled}, } for i, tt := range tests { - if got, want := tbl.Lookup(tt.req, "", rndPicker, prefixMatcher).URL.String(), tt.dst; got != want { + if got, want := tbl.Lookup(tt.req, "", rndPicker, prefixMatcher, tt.globEnabled).URL.String(), tt.dst; got != want { t.Errorf("%d: got %v want %v", i, got, want) } }