From cf30c15ff67b2d0e26916b98bdeb21ba09fa270f Mon Sep 17 00:00:00 2001 From: Chris Lajoie Date: Tue, 28 Nov 2017 07:25:48 -0700 Subject: [PATCH 1/6] Implement redirection capability --- admin/api/routes.go | 3 +++ proxy/http_integration_test.go | 39 ++++++++++++++++++++++++++++++++++ proxy/http_proxy.go | 9 ++++++++ registry/consul/parse_test.go | 6 ++++++ registry/consul/service.go | 2 ++ route/route.go | 11 ++++++++-- route/table.go | 9 ++++---- route/table_test.go | 9 +++++++- route/target.go | 3 +++ 9 files changed, 84 insertions(+), 7 deletions(-) diff --git a/admin/api/routes.go b/admin/api/routes.go index 10c2ac373..54a9aba8a 100644 --- a/admin/api/routes.go +++ b/admin/api/routes.go @@ -62,6 +62,9 @@ func (h *RoutesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { Rate1: tg.Timer.Rate1(), Pct99: tg.Timer.Percentile(0.99), } + if tg.RedirectCode != 0 { + ar.Dst = fmt.Sprintf("%d|%s", tg.RedirectCode, tg.URL) + } routes = append(routes, ar) } } diff --git a/proxy/http_integration_test.go b/proxy/http_integration_test.go index ff3627398..7c49bcd6a 100644 --- a/proxy/http_integration_test.go +++ b/proxy/http_integration_test.go @@ -177,6 +177,45 @@ func TestProxyHost(t *testing.T) { }) } +func TestRedirect(t *testing.T) { + routes := "route add mock /foo http://a.com/abc opts \"redirect=301\"\n" + routes += "route add mock /bar http://b.com/ opts \"redirect=302\"\n" + tbl, _ := route.NewTable(routes) + + proxy := httptest.NewServer(&HTTPProxy{ + Transport: http.DefaultTransport, + Lookup: func(r *http.Request) *route.Target { + return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) + }, + }) + defer proxy.Close() + + tests := []struct { + req string + wantCode int + wantLoc string + }{ + {req: "/foo", wantCode: 301, wantLoc: "http://a.com/abc"}, + {req: "/bar", wantCode: 302, wantLoc: "http://b.com"}, + } + + http.DefaultClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + // do not follow redirects + return http.ErrUseLastResponse + } + + for _, tt := range tests { + resp, _ := mustGet(proxy.URL + tt.req) + if resp.StatusCode != tt.wantCode { + t.Errorf("got status code %d, want %d", resp.StatusCode, tt.wantCode) + } + gotLoc, _ := resp.Location() + if gotLoc.String() != tt.wantLoc { + t.Errorf("got location %s, want %s", gotLoc, tt.wantLoc) + } + } +} + func TestProxyLogOutput(t *testing.T) { // build a format string from all log fields and one header field fields := []string{"header.X-Foo:$header.X-Foo"} diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index eb14b0c52..714f6c227 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -114,6 +114,15 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { r.Header.Set(p.Config.RequestID, id()) } + if t.RedirectCode != 0 { + http.Redirect(w, r, targetURL.String(), t.RedirectCode) + if t.Timer != nil { + t.Timer.Update(0) + } + metrics.DefaultRegistry.GetTimer(key(t.RedirectCode)).Update(0) + return + } + upgrade, accept := r.Header.Get("Upgrade"), r.Header.Get("Accept") tr := p.Transport diff --git a/registry/consul/parse_test.go b/registry/consul/parse_test.go index 38d3ae274..d1528ba3a 100644 --- a/registry/consul/parse_test.go +++ b/registry/consul/parse_test.go @@ -47,6 +47,12 @@ func TestParseTag(t *testing.T) { route: "xx/Yy", ok: true, }, + { + tag: "p-www.bar.com:80/foo https://www.bar.com/ redirect=302", + route: "www.bar.com:80/foo", + opts: "https://www.bar.com/ redirect=302", + ok: true, + }, } for i, tt := range tests { diff --git a/registry/consul/service.go b/registry/consul/service.go index 7b6d787b3..db0d04418 100644 --- a/registry/consul/service.go +++ b/registry/consul/service.go @@ -119,6 +119,8 @@ func serviceConfig(client *api.Client, name string, passing map[string]bool, tag dst := "http://" + addr + "/" for _, o := range strings.Fields(opts) { switch { + case strings.Contains(o, "://"): + dst = o case o == "proto=tcp": dst = "tcp://" + addr case o == "proto=https": diff --git a/route/route.go b/route/route.go index 152c4d30e..c70f91ca6 100644 --- a/route/route.go +++ b/route/route.go @@ -6,6 +6,7 @@ import ( "net/url" "reflect" "sort" + "strconv" "strings" "github.com/fabiolb/fabio/metrics" @@ -68,6 +69,7 @@ func (r *Route) addTarget(service string, targetURL *url.URL, fixedWeight float6 t.StripPath = opts["strip"] t.TLSSkipVerify = opts["tlsskipverify"] == "true" t.Host = opts["host"] + t.RedirectCode, _ = strconv.Atoi(opts["redirect"]) } r.Targets = append(r.Targets, t) @@ -134,7 +136,12 @@ func contains(src, dst []string) bool { } func (r *Route) TargetConfig(t *Target, addWeight bool) string { - s := fmt.Sprintf("route add %s %s %s", t.Service, r.Host+r.Path, t.URL) + s := fmt.Sprintf("route add %s %s", t.Service, r.Host+r.Path) + if t.RedirectCode != 0 { + s += fmt.Sprintf(" %d|%s", t.RedirectCode, t.URL) + } else { + s += fmt.Sprintf(" %s", t.URL) + } if addWeight { s += fmt.Sprintf(" weight %2.4f", t.Weight) } else if t.FixedWeight > 0 { @@ -215,7 +222,7 @@ func (r *Route) weighTargets() { } // compute the weight for the targets with dynamic weights - dynamic := (1 - sumFixed) / float64(len(r.Targets)-nFixed) + dynamic := float64(1-sumFixed) / float64(len(r.Targets)-nFixed) if dynamic < 0 { dynamic = 0 } diff --git a/route/table.go b/route/table.go index 054185eb5..03438e12b 100644 --- a/route/table.go +++ b/route/table.go @@ -273,8 +273,8 @@ func (t Table) route(host, path string) *Route { // normalizeHost returns the hostname from the request // and removes the default port if present. -func normalizeHost(req *http.Request) string { - host := strings.ToLower(req.Host) +func normalizeHost(host string, req *http.Request) string { + host = strings.ToLower(host) if req.TLS == nil && strings.HasSuffix(host, ":80") { return host[:len(host)-len(":80")] } @@ -287,9 +287,10 @@ func normalizeHost(req *http.Request) string { // matchingHosts returns all keys (host name patterns) from the // routing table which match the normalized request hostname. func (t Table) matchingHosts(req *http.Request) (hosts []string) { - host := normalizeHost(req) + host := normalizeHost(req.Host, req) for pattern := range t { - if glob.Glob(pattern, host) { + normpat := normalizeHost(pattern, req) + if glob.Glob(normpat, host) { hosts = append(hosts, pattern) } } diff --git a/route/table_test.go b/route/table_test.go index c631cad37..41af91c36 100644 --- a/route/table_test.go +++ b/route/table_test.go @@ -416,6 +416,9 @@ func TestTableParse(t *testing.T) { targetURLs := make([]string, len(r.wTargets)) for i, tg := range r.wTargets { targetURLs[i] = tg.URL.Scheme + "://" + tg.URL.Host + tg.URL.Path + if tg.RedirectCode != 0 { + targetURLs[i] = fmt.Sprintf("%d|%s", tg.RedirectCode, targetURLs[i]) + } } // count how often the 'url' from 'route add svc ' @@ -477,7 +480,7 @@ func TestNormalizeHost(t *testing.T) { } for i, tt := range tests { - if got, want := normalizeHost(tt.req), tt.host; got != want { + if got, want := normalizeHost(tt.req.Host, tt.req), tt.host; got != want { t.Errorf("%d: got %v want %v", i, got, want) } } @@ -495,6 +498,7 @@ func TestTableLookup(t *testing.T) { route add svc z.abc.com/foo/ http://foo.com:3100 route add svc *.abc.com/ http://foo.com:4000 route add svc *.abc.com/foo/ http://foo.com:5000 + route add svc xyz.com:80/ https://xyz.com ` tbl, err := NewTable(s) @@ -539,6 +543,9 @@ func TestTableLookup(t *testing.T) { // exact match has precedence over glob match {&http.Request{Host: "z.abc.com", URL: mustParse("/foo/")}, "http://foo.com:3100"}, + + // explicit port on route + {&http.Request{Host: "xyz.com", URL: mustParse("/")}, "https://xyz.com"}, } for i, tt := range tests { diff --git a/route/target.go b/route/target.go index 14b0a3a45..9718e8ae8 100644 --- a/route/target.go +++ b/route/target.go @@ -33,6 +33,9 @@ type Target struct { // URL is the endpoint the service instance listens on URL *url.URL + // RedirectCode is the HTTP status code used for redirects. + RedirectCode int + // FixedWeight is the weight assigned to this target. // If the value is 0 the targets weight is dynamic. FixedWeight float64 From 2f2ce0831907a4289685e3b3f8ea1a08091abd8c Mon Sep 17 00:00:00 2001 From: Chris Lajoie Date: Tue, 28 Nov 2017 18:11:21 -0700 Subject: [PATCH 2/6] Ensure RedirectCode is somewhere in the 3xx range --- route/route.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/route/route.go b/route/route.go index c70f91ca6..f3b9be86f 100644 --- a/route/route.go +++ b/route/route.go @@ -70,6 +70,9 @@ func (r *Route) addTarget(service string, targetURL *url.URL, fixedWeight float6 t.TLSSkipVerify = opts["tlsskipverify"] == "true" t.Host = opts["host"] t.RedirectCode, _ = strconv.Atoi(opts["redirect"]) + if t.RedirectCode < 300 || t.RedirectCode > 399 { + t.RedirectCode = 0 + } } r.Targets = append(r.Targets, t) From 1da6bff84c70f495dec5d09167bdeef29b0fa468 Mon Sep 17 00:00:00 2001 From: Chris Lajoie Date: Wed, 29 Nov 2017 13:13:41 -0700 Subject: [PATCH 3/6] Implement suggestions. General cleanup. --- admin/api/routes.go | 3 --- proxy/http_integration_test.go | 6 ++++-- registry/consul/parse_test.go | 4 ++-- registry/consul/service.go | 12 ++++++++++-- route/route.go | 21 +++++++++++---------- route/table.go | 10 +++++----- route/table_test.go | 3 --- route/target.go | 1 + 8 files changed, 33 insertions(+), 27 deletions(-) diff --git a/admin/api/routes.go b/admin/api/routes.go index 54a9aba8a..10c2ac373 100644 --- a/admin/api/routes.go +++ b/admin/api/routes.go @@ -62,9 +62,6 @@ func (h *RoutesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { Rate1: tg.Timer.Rate1(), Pct99: tg.Timer.Percentile(0.99), } - if tg.RedirectCode != 0 { - ar.Dst = fmt.Sprintf("%d|%s", tg.RedirectCode, tg.URL) - } routes = append(routes, ar) } } diff --git a/proxy/http_integration_test.go b/proxy/http_integration_test.go index 7c49bcd6a..1762035e3 100644 --- a/proxy/http_integration_test.go +++ b/proxy/http_integration_test.go @@ -178,8 +178,9 @@ func TestProxyHost(t *testing.T) { } func TestRedirect(t *testing.T) { - routes := "route add mock /foo http://a.com/abc opts \"redirect=301\"\n" - routes += "route add mock /bar http://b.com/ opts \"redirect=302\"\n" + routes := "route add mock / http://a.com opts \"redirect=301\"\n" + routes += "route add mock /foo http://a.com/abc opts \"redirect=301\"\n" + routes += "route add mock /bar http://b.com opts \"redirect=302\"\n" tbl, _ := route.NewTable(routes) proxy := httptest.NewServer(&HTTPProxy{ @@ -195,6 +196,7 @@ func TestRedirect(t *testing.T) { wantCode int wantLoc string }{ + {req: "/", wantCode: 301, wantLoc: "http://a.com/"}, {req: "/foo", wantCode: 301, wantLoc: "http://a.com/abc"}, {req: "/bar", wantCode: 302, wantLoc: "http://b.com"}, } diff --git a/registry/consul/parse_test.go b/registry/consul/parse_test.go index d1528ba3a..6c3054a02 100644 --- a/registry/consul/parse_test.go +++ b/registry/consul/parse_test.go @@ -48,9 +48,9 @@ func TestParseTag(t *testing.T) { ok: true, }, { - tag: "p-www.bar.com:80/foo https://www.bar.com/ redirect=302", + tag: "p-www.bar.com:80/foo redirect=302,https://www.bar.com", route: "www.bar.com:80/foo", - opts: "https://www.bar.com/ redirect=302", + opts: "redirect=302,https://www.bar.com", ok: true, }, } diff --git a/registry/consul/service.go b/registry/consul/service.go index db0d04418..98df8fceb 100644 --- a/registry/consul/service.go +++ b/registry/consul/service.go @@ -1,6 +1,7 @@ package consul import ( + "fmt" "log" "net" "runtime" @@ -119,14 +120,21 @@ func serviceConfig(client *api.Client, name string, passing map[string]bool, tag dst := "http://" + addr + "/" for _, o := range strings.Fields(opts) { switch { - case strings.Contains(o, "://"): - dst = o case o == "proto=tcp": dst = "tcp://" + addr case o == "proto=https": dst = "https://" + addr case strings.HasPrefix(o, "weight="): weight = o[len("weight="):] + case strings.HasPrefix(o, "redirect="): + redir := strings.Split(o[len("redirect="):], ",") + if len(redir) == 2 { + dst = redir[1] + ropts = append(ropts, fmt.Sprintf("redirect=%s", redir[0])) + } else { + log.Printf("[ERROR] Invalid syntax for redirect: %s. should be redirect=,", o) + continue + } default: ropts = append(ropts, o) } diff --git a/route/route.go b/route/route.go index f3b9be86f..a9420eaea 100644 --- a/route/route.go +++ b/route/route.go @@ -69,9 +69,15 @@ func (r *Route) addTarget(service string, targetURL *url.URL, fixedWeight float6 t.StripPath = opts["strip"] t.TLSSkipVerify = opts["tlsskipverify"] == "true" t.Host = opts["host"] - t.RedirectCode, _ = strconv.Atoi(opts["redirect"]) - if t.RedirectCode < 300 || t.RedirectCode > 399 { - t.RedirectCode = 0 + + if opts["redirect"] != "" { + t.RedirectCode, err = strconv.Atoi(opts["redirect"]) + if err != nil { + log.Printf("[ERROR] redirect status code should be numeric in 3xx range. Got: %s", opts["redirect"]) + } else if t.RedirectCode < 300 || t.RedirectCode > 399 { + t.RedirectCode = 0 + log.Printf("[ERROR] redirect status code should in 3xx range. Got: %s", opts["redirect"]) + } } } @@ -139,12 +145,7 @@ func contains(src, dst []string) bool { } func (r *Route) TargetConfig(t *Target, addWeight bool) string { - s := fmt.Sprintf("route add %s %s", t.Service, r.Host+r.Path) - if t.RedirectCode != 0 { - s += fmt.Sprintf(" %d|%s", t.RedirectCode, t.URL) - } else { - s += fmt.Sprintf(" %s", t.URL) - } + s := fmt.Sprintf("route add %s %s %s", t.Service, r.Host+r.Path, t.URL) if addWeight { s += fmt.Sprintf(" weight %2.4f", t.Weight) } else if t.FixedWeight > 0 { @@ -225,7 +226,7 @@ func (r *Route) weighTargets() { } // compute the weight for the targets with dynamic weights - dynamic := float64(1-sumFixed) / float64(len(r.Targets)-nFixed) + dynamic := (1 - sumFixed) / float64(len(r.Targets)-nFixed) if dynamic < 0 { dynamic = 0 } diff --git a/route/table.go b/route/table.go index 03438e12b..6a23021d4 100644 --- a/route/table.go +++ b/route/table.go @@ -273,12 +273,12 @@ func (t Table) route(host, path string) *Route { // normalizeHost returns the hostname from the request // and removes the default port if present. -func normalizeHost(host string, req *http.Request) string { +func normalizeHost(host string, tls bool) string { host = strings.ToLower(host) - if req.TLS == nil && strings.HasSuffix(host, ":80") { + if !tls && strings.HasSuffix(host, ":80") { return host[:len(host)-len(":80")] } - if req.TLS != nil && strings.HasSuffix(host, ":443") { + if tls && strings.HasSuffix(host, ":443") { return host[:len(host)-len(":443")] } return host @@ -287,9 +287,9 @@ func normalizeHost(host string, req *http.Request) string { // matchingHosts returns all keys (host name patterns) from the // routing table which match the normalized request hostname. func (t Table) matchingHosts(req *http.Request) (hosts []string) { - host := normalizeHost(req.Host, req) + host := normalizeHost(req.Host, req.TLS != nil) for pattern := range t { - normpat := normalizeHost(pattern, req) + normpat := normalizeHost(pattern, req.TLS != nil) if glob.Glob(normpat, host) { hosts = append(hosts, pattern) } diff --git a/route/table_test.go b/route/table_test.go index 41af91c36..2e67870cc 100644 --- a/route/table_test.go +++ b/route/table_test.go @@ -416,9 +416,6 @@ func TestTableParse(t *testing.T) { targetURLs := make([]string, len(r.wTargets)) for i, tg := range r.wTargets { targetURLs[i] = tg.URL.Scheme + "://" + tg.URL.Host + tg.URL.Path - if tg.RedirectCode != 0 { - targetURLs[i] = fmt.Sprintf("%d|%s", tg.RedirectCode, targetURLs[i]) - } } // count how often the 'url' from 'route add svc ' diff --git a/route/target.go b/route/target.go index 9718e8ae8..65adb5ee2 100644 --- a/route/target.go +++ b/route/target.go @@ -34,6 +34,7 @@ type Target struct { URL *url.URL // RedirectCode is the HTTP status code used for redirects. + // When set to a value > 0 the client is redirected to the target url. RedirectCode int // FixedWeight is the weight assigned to this target. From d1259f8af9922d1401d587dbce29238b0898d26d Mon Sep 17 00:00:00 2001 From: Chris Lajoie Date: Wed, 29 Nov 2017 13:24:12 -0700 Subject: [PATCH 4/6] typo --- route/route.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/route/route.go b/route/route.go index a9420eaea..f0659c923 100644 --- a/route/route.go +++ b/route/route.go @@ -76,7 +76,7 @@ func (r *Route) addTarget(service string, targetURL *url.URL, fixedWeight float6 log.Printf("[ERROR] redirect status code should be numeric in 3xx range. Got: %s", opts["redirect"]) } else if t.RedirectCode < 300 || t.RedirectCode > 399 { t.RedirectCode = 0 - log.Printf("[ERROR] redirect status code should in 3xx range. Got: %s", opts["redirect"]) + log.Printf("[ERROR] redirect status code should be in 3xx range. Got: %s", opts["redirect"]) } } } From 1e83fa78d939a4bdfccdb180e13a515cdd63eb23 Mon Sep 17 00:00:00 2001 From: Chris Lajoie Date: Wed, 29 Nov 2017 13:38:36 -0700 Subject: [PATCH 5/6] Fix TestNormalizeHost --- route/table_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/route/table_test.go b/route/table_test.go index 2e67870cc..eb0fdea71 100644 --- a/route/table_test.go +++ b/route/table_test.go @@ -477,7 +477,7 @@ func TestNormalizeHost(t *testing.T) { } for i, tt := range tests { - if got, want := normalizeHost(tt.req.Host, tt.req), tt.host; got != want { + if got, want := normalizeHost(tt.req.Host, tt.req.TLS != nil), tt.host; got != want { t.Errorf("%d: got %v want %v", i, got, want) } } From 1d78ef81ac177bdf3fed894f055feb73548ecfe4 Mon Sep 17 00:00:00 2001 From: Chris Lajoie Date: Thu, 30 Nov 2017 12:45:50 -0700 Subject: [PATCH 6/6] Implement $path pseudo-variable for redirect targets --- proxy/http_integration_test.go | 6 +- proxy/http_proxy.go | 35 +++++------ route/target.go | 27 +++++++++ route/target_test.go | 104 +++++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 19 deletions(-) create mode 100644 route/target_test.go diff --git a/proxy/http_integration_test.go b/proxy/http_integration_test.go index 1762035e3..19973386b 100644 --- a/proxy/http_integration_test.go +++ b/proxy/http_integration_test.go @@ -178,9 +178,9 @@ func TestProxyHost(t *testing.T) { } func TestRedirect(t *testing.T) { - routes := "route add mock / http://a.com opts \"redirect=301\"\n" + routes := "route add mock / http://a.com/$path opts \"redirect=301\"\n" routes += "route add mock /foo http://a.com/abc opts \"redirect=301\"\n" - routes += "route add mock /bar http://b.com opts \"redirect=302\"\n" + routes += "route add mock /bar http://b.com/$path opts \"redirect=302 strip=/bar\"\n" tbl, _ := route.NewTable(routes) proxy := httptest.NewServer(&HTTPProxy{ @@ -197,8 +197,10 @@ func TestRedirect(t *testing.T) { wantLoc string }{ {req: "/", wantCode: 301, wantLoc: "http://a.com/"}, + {req: "/aaa/bbb", wantCode: 301, wantLoc: "http://a.com/aaa/bbb"}, {req: "/foo", wantCode: 301, wantLoc: "http://a.com/abc"}, {req: "/bar", wantCode: 302, wantLoc: "http://b.com"}, + {req: "/bar/aaa", wantCode: 302, wantLoc: "http://b.com/aaa"}, } http.DefaultClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index 714f6c227..2c17237d0 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -60,6 +60,14 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { panic("no lookup function") } + if p.Config.RequestID != "" { + id := p.UUID + if id == nil { + id = uuid.NewUUID + } + r.Header.Set(p.Config.RequestID, id()) + } + t := p.Lookup(r) if t == nil { w.WriteHeader(p.Config.NoRouteStatus) @@ -75,6 +83,16 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { RawQuery: r.URL.RawQuery, } + if t.RedirectCode != 0 { + redirectURL := t.GetRedirectURL(requestURL) + http.Redirect(w, r, redirectURL.String(), t.RedirectCode) + if t.Timer != nil { + t.Timer.Update(0) + } + metrics.DefaultRegistry.GetTimer(key(t.RedirectCode)).Update(0) + return + } + // build the real target url that is passed to the proxy targetURL := &url.URL{ Scheme: t.URL.Scheme, @@ -106,23 +124,6 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if p.Config.RequestID != "" { - id := p.UUID - if id == nil { - id = uuid.NewUUID - } - r.Header.Set(p.Config.RequestID, id()) - } - - if t.RedirectCode != 0 { - http.Redirect(w, r, targetURL.String(), t.RedirectCode) - if t.Timer != nil { - t.Timer.Update(0) - } - metrics.DefaultRegistry.GetTimer(key(t.RedirectCode)).Update(0) - return - } - upgrade, accept := r.Header.Get("Upgrade"), r.Header.Get("Accept") tr := p.Transport diff --git a/route/target.go b/route/target.go index 65adb5ee2..6f5a70034 100644 --- a/route/target.go +++ b/route/target.go @@ -2,6 +2,7 @@ package route import ( "net/url" + "strings" "github.com/fabiolb/fabio/metrics" ) @@ -50,3 +51,29 @@ type Target struct { // TimerName is the name of the timer in the metrics registry TimerName string } + +func (t *Target) GetRedirectURL(requestURL *url.URL) *url.URL { + redirectURL := &url.URL{ + Scheme: t.URL.Scheme, + Host: t.URL.Host, + Path: t.URL.Path, + RawQuery: t.URL.RawQuery, + } + if strings.HasSuffix(redirectURL.Host, "$path") { + redirectURL.Host = redirectURL.Host[:len(redirectURL.Host)-len("$path")] + redirectURL.Path = "$path" + } + if strings.Contains(redirectURL.Path, "/$path") { + redirectURL.Path = strings.Replace(redirectURL.Path, "/$path", "$path", 1) + } + if strings.Contains(redirectURL.Path, "$path") { + redirectURL.Path = strings.Replace(redirectURL.Path, "$path", requestURL.Path, 1) + if t.StripPath != "" && strings.HasPrefix(redirectURL.Path, t.StripPath) { + redirectURL.Path = redirectURL.Path[len(t.StripPath):] + } + if redirectURL.RawQuery == "" && requestURL.RawQuery != "" { + redirectURL.RawQuery = requestURL.RawQuery + } + } + return redirectURL +} diff --git a/route/target_test.go b/route/target_test.go new file mode 100644 index 000000000..7d9c7794b --- /dev/null +++ b/route/target_test.go @@ -0,0 +1,104 @@ +package route + +import ( + "net/url" + "testing" +) + +func TestTarget_GetRedirectURL(t *testing.T) { + type routeTest struct { + req string + want string + } + tests := []struct { + route string + tests []routeTest + }{ + { // simple absolute redirect + route: "route add svc / http://bar.com/", + tests: []routeTest{ + {req: "/", want: "http://bar.com/"}, + {req: "/abc", want: "http://bar.com/"}, + {req: "/a/b/c", want: "http://bar.com/"}, + {req: "/?aaa=1", want: "http://bar.com/"}, + }, + }, + { // absolute redirect to deep path with query + route: "route add svc / http://bar.com/a/b/c?foo=bar", + tests: []routeTest{ + {req: "/", want: "http://bar.com/a/b/c?foo=bar"}, + {req: "/abc", want: "http://bar.com/a/b/c?foo=bar"}, + {req: "/a/b/c", want: "http://bar.com/a/b/c?foo=bar"}, + {req: "/?aaa=1", want: "http://bar.com/a/b/c?foo=bar"}, + }, + }, + { // simple redirect to corresponding path + route: "route add svc / http://bar.com/$path", + tests: []routeTest{ + {req: "/", want: "http://bar.com/"}, + {req: "/abc", want: "http://bar.com/abc"}, + {req: "/a/b/c", want: "http://bar.com/a/b/c"}, + {req: "/?aaa=1", want: "http://bar.com/?aaa=1"}, + {req: "/abc/?aaa=1", want: "http://bar.com/abc/?aaa=1"}, + }, + }, + { // same as above but without / before $path + route: "route add svc / http://bar.com$path", + tests: []routeTest{ + {req: "/", want: "http://bar.com/"}, + {req: "/abc", want: "http://bar.com/abc"}, + {req: "/a/b/c", want: "http://bar.com/a/b/c"}, + {req: "/?aaa=1", want: "http://bar.com/?aaa=1"}, + {req: "/abc/?aaa=1", want: "http://bar.com/abc/?aaa=1"}, + }, + }, + { // arbitrary subdir on target with $path at end + route: "route add svc / http://bar.com/bbb/$path", + tests: []routeTest{ + {req: "/", want: "http://bar.com/bbb/"}, + {req: "/abc", want: "http://bar.com/bbb/abc"}, + {req: "/a/b/c", want: "http://bar.com/bbb/a/b/c"}, + {req: "/?aaa=1", want: "http://bar.com/bbb/?aaa=1"}, + {req: "/abc/?aaa=1", want: "http://bar.com/bbb/abc/?aaa=1"}, + }, + }, + { // same as above but without / before $path + route: "route add svc / http://bar.com/bbb$path", + tests: []routeTest{ + {req: "/", want: "http://bar.com/bbb/"}, + {req: "/abc", want: "http://bar.com/bbb/abc"}, + {req: "/a/b/c", want: "http://bar.com/bbb/a/b/c"}, + {req: "/?aaa=1", want: "http://bar.com/bbb/?aaa=1"}, + {req: "/abc/?aaa=1", want: "http://bar.com/bbb/abc/?aaa=1"}, + }, + }, + { // strip prefix + route: "route add svc /stripme http://bar.com/$path opts \"strip=/stripme\"", + tests: []routeTest{ + {req: "/stripme/", want: "http://bar.com/"}, + {req: "/stripme/abc", want: "http://bar.com/abc"}, + {req: "/stripme/a/b/c", want: "http://bar.com/a/b/c"}, + {req: "/stripme/?aaa=1", want: "http://bar.com/?aaa=1"}, + {req: "/stripme/abc/?aaa=1", want: "http://bar.com/abc/?aaa=1"}, + }, + }, + } + firstRoute := func(tbl Table) *Route { + for _, routes := range tbl { + return routes[0] + } + return nil + } + for _, tt := range tests { + tbl, _ := NewTable(tt.route) + route := firstRoute(tbl) + target := route.Targets[0] + for _, rt := range tt.tests { + reqURL, _ := url.Parse("http://foo.com" + rt.req) + got := target.GetRedirectURL(reqURL) + if got.String() != rt.want { + t.Errorf("Got %s, wanted %s", got, rt.want) + } + } + } +}