Commit 345ece38 authored by Viacheslav Biriukov's avatar Viacheslav Biriukov Committed by Matthew Holt

add multi proxy supprot based on urls

parent 2b44a7d0
......@@ -77,86 +77,102 @@ var tryDuration = 60 * time.Second
// ServeHTTP satisfies the httpserver.Handler interface.
func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for _, upstream := range p.Upstreams {
if !httpserver.Path(r.URL.Path).Matches(upstream.From()) ||
!upstream.AllowedPath(r.URL.Path) {
continue
}
// Start by selecting most specific matching upstream config
upstream := p.match(r)
if upstream == nil {
return p.Next.ServeHTTP(w, r)
}
var replacer httpserver.Replacer
start := time.Now()
var replacer httpserver.Replacer
start := time.Now()
outreq := createUpstreamRequest(r)
outreq := createUpstreamRequest(r)
// Since Select() should give us "up" hosts, keep retrying
// hosts until timeout (or until we get a nil host).
for time.Now().Sub(start) < tryDuration {
host := upstream.Select()
if host == nil {
return http.StatusBadGateway, errUnreachable
// Since Select() should give us "up" hosts, keep retrying
// hosts until timeout (or until we get a nil host).
for time.Now().Sub(start) < tryDuration {
host := upstream.Select()
if host == nil {
return http.StatusBadGateway, errUnreachable
}
if rr, ok := w.(*httpserver.ResponseRecorder); ok && rr.Replacer != nil {
rr.Replacer.Set("upstream", host.Name)
}
outreq.Host = host.Name
if host.UpstreamHeaders != nil {
if replacer == nil {
rHost := r.Host
replacer = httpserver.NewReplacer(r, nil, "")
outreq.Host = rHost
}
if rr, ok := w.(*httpserver.ResponseRecorder); ok && rr.Replacer != nil {
rr.Replacer.Set("upstream", host.Name)
if v, ok := host.UpstreamHeaders["Host"]; ok {
outreq.Host = replacer.Replace(v[len(v)-1])
}
outreq.Host = host.Name
if host.UpstreamHeaders != nil {
if replacer == nil {
rHost := r.Host
replacer = httpserver.NewReplacer(r, nil, "")
outreq.Host = rHost
}
if v, ok := host.UpstreamHeaders["Host"]; ok {
outreq.Host = replacer.Replace(v[len(v)-1])
}
// Modify headers for request that will be sent to the upstream host
upHeaders := createHeadersByRules(host.UpstreamHeaders, r.Header, replacer)
for k, v := range upHeaders {
outreq.Header[k] = v
}
// Modify headers for request that will be sent to the upstream host
upHeaders := createHeadersByRules(host.UpstreamHeaders, r.Header, replacer)
for k, v := range upHeaders {
outreq.Header[k] = v
}
}
var downHeaderUpdateFn respUpdateFn
if host.DownstreamHeaders != nil {
if replacer == nil {
rHost := r.Host
replacer = httpserver.NewReplacer(r, nil, "")
outreq.Host = rHost
}
//Creates a function that is used to update headers the response received by the reverse proxy
downHeaderUpdateFn = createRespHeaderUpdateFn(host.DownstreamHeaders, replacer)
var downHeaderUpdateFn respUpdateFn
if host.DownstreamHeaders != nil {
if replacer == nil {
rHost := r.Host
replacer = httpserver.NewReplacer(r, nil, "")
outreq.Host = rHost
}
//Creates a function that is used to update headers the response received by the reverse proxy
downHeaderUpdateFn = createRespHeaderUpdateFn(host.DownstreamHeaders, replacer)
}
proxy := host.ReverseProxy
if baseURL, err := url.Parse(host.Name); err == nil {
r.Host = baseURL.Host
if proxy == nil {
proxy = NewSingleHostReverseProxy(baseURL, host.WithoutPathPrefix)
}
} else if proxy == nil {
return http.StatusInternalServerError, err
proxy := host.ReverseProxy
if baseURL, err := url.Parse(host.Name); err == nil {
r.Host = baseURL.Host
if proxy == nil {
proxy = NewSingleHostReverseProxy(baseURL, host.WithoutPathPrefix)
}
} else if proxy == nil {
return http.StatusInternalServerError, err
}
atomic.AddInt64(&host.Conns, 1)
backendErr := proxy.ServeHTTP(w, outreq, downHeaderUpdateFn)
atomic.AddInt64(&host.Conns, -1)
if backendErr == nil {
return 0, nil
}
timeout := host.FailTimeout
if timeout == 0 {
timeout = 10 * time.Second
}
atomic.AddInt32(&host.Fails, 1)
go func(host *UpstreamHost, timeout time.Duration) {
time.Sleep(timeout)
atomic.AddInt32(&host.Fails, -1)
}(host, timeout)
atomic.AddInt64(&host.Conns, 1)
backendErr := proxy.ServeHTTP(w, outreq, downHeaderUpdateFn)
atomic.AddInt64(&host.Conns, -1)
if backendErr == nil {
return 0, nil
}
timeout := host.FailTimeout
if timeout == 0 {
timeout = 10 * time.Second
}
return http.StatusBadGateway, errUnreachable
atomic.AddInt32(&host.Fails, 1)
go func(host *UpstreamHost, timeout time.Duration) {
time.Sleep(timeout)
atomic.AddInt32(&host.Fails, -1)
}(host, timeout)
}
return p.Next.ServeHTTP(w, r)
return http.StatusBadGateway, errUnreachable
}
// match finds the best match for a proxy config based
// on r.
func (p Proxy) match(r *http.Request) Upstream {
var u Upstream
var longestMatch int
for _, upstream := range p.Upstreams {
basePath := upstream.From()
if !httpserver.Path(r.URL.Path).Matches(basePath) || !upstream.AllowedPath(r.URL.Path) {
continue
}
if len(basePath) > longestMatch {
longestMatch = len(basePath)
u = upstream
}
}
return u
}
// createUpstremRequest shallow-copies r into a new request
......
......@@ -40,6 +40,7 @@ func TestReverseProxy(t *testing.T) {
// set up proxy
p := &Proxy{
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
Upstreams: []Upstream{newFakeUpstream(backend.URL, false)},
}
......@@ -80,6 +81,7 @@ func TestReverseProxyInsecureSkipVerify(t *testing.T) {
// set up proxy
p := &Proxy{
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
Upstreams: []Upstream{newFakeUpstream(backend.URL, true)},
}
......@@ -372,6 +374,7 @@ func TestUpstreamHeadersUpdate(t *testing.T) {
}
// set up proxy
p := &Proxy{
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
Upstreams: []Upstream{upstream},
}
......@@ -441,6 +444,7 @@ func TestDownstreamHeadersUpdate(t *testing.T) {
}
// set up proxy
p := &Proxy{
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
Upstreams: []Upstream{upstream},
}
......@@ -485,10 +489,98 @@ func TestDownstreamHeadersUpdate(t *testing.T) {
}
var (
upstreamResp1 = []byte("Hello, /")
upstreamResp2 = []byte("Hello, /api/")
)
func newMultiHostTestProxy() *Proxy {
// No-op backends.
upstreamServer1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s", upstreamResp1)
}))
upstreamServer2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s", upstreamResp2)
}))
p := &Proxy{
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
Upstreams: []Upstream{
// The order is important; the short path should go first to ensure
// we choose the most specific route, not the first one.
&fakeUpstream{
name: upstreamServer1.URL,
from: "/",
},
&fakeUpstream{
name: upstreamServer2.URL,
from: "/api",
},
},
}
return p
}
func TestMultiReverseProxyFromClient(t *testing.T) {
p := newMultiHostTestProxy()
// This is a full end-end test, so the proxy handler.
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p.ServeHTTP(w, r)
}))
defer proxy.Close()
// Table tests.
var multiProxy = []struct {
url string
body []byte
}{
{
"/",
upstreamResp1,
},
{
"/api/",
upstreamResp2,
},
{
"/messages/",
upstreamResp1,
},
{
"/api/messages/?text=cat",
upstreamResp2,
},
}
for _, tt := range multiProxy {
// Create client request
reqURL := singleJoiningSlash(proxy.URL, tt.url)
req, err := http.NewRequest("GET", reqURL, nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("Failed to make request: %v", err)
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
t.Fatalf("Failed to read response: %v", err)
}
if !bytes.Equal(body, tt.body) {
t.Errorf("Expected '%s' but got '%s' instead", tt.body, body)
}
}
}
func newFakeUpstream(name string, insecure bool) *fakeUpstream {
uri, _ := url.Parse(name)
u := &fakeUpstream{
name: name,
from: "/",
host: &UpstreamHost{
Name: name,
ReverseProxy: NewSingleHostReverseProxy(uri, ""),
......@@ -501,15 +593,27 @@ func newFakeUpstream(name string, insecure bool) *fakeUpstream {
}
type fakeUpstream struct {
name string
host *UpstreamHost
name string
host *UpstreamHost
from string
without string
}
func (u *fakeUpstream) From() string {
return "/"
return u.from
}
func (u *fakeUpstream) Select() *UpstreamHost {
if u.host == nil {
uri, err := url.Parse(u.name)
if err != nil {
log.Fatalf("Unable to url.Parse %s: %v", u.name, err)
}
u.host = &UpstreamHost{
Name: u.name,
ReverseProxy: NewSingleHostReverseProxy(uri, u.without),
}
}
return u.host
}
......@@ -523,12 +627,14 @@ func (u *fakeUpstream) AllowedPath(requestPath string) bool {
// proxy.
func newWebSocketTestProxy(backendAddr string) *Proxy {
return &Proxy{
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
Upstreams: []Upstream{&fakeWsUpstream{name: backendAddr, without: ""}},
}
}
func newPrefixedWebSocketTestProxy(backendAddr string, prefix string) *Proxy {
return &Proxy{
Next: httpserver.EmptyNext, // prevents panic in some cases when test fails
Upstreams: []Upstream{&fakeWsUpstream{name: backendAddr, without: prefix}},
}
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment