Skip to content

Commit

Permalink
refactor: extract ProxyForHost logic to interface (#40)
Browse files Browse the repository at this point in the history
* refactor: extract ProxyForHost logic to interface

this pure refactor moves the logic that builds the map of host -> backend
url into an interface that could be implemented differently in the future.

a new type of `Proxies` will be used to do height-based sharding of nodes
based on the height of the incoming request.

* add tests for Proxies

* add proxy middleware config docs
  • Loading branch information
pirtleshell authored Oct 10, 2023
1 parent 69b86f0 commit 2f8fa7d
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 12 deletions.
22 changes: 22 additions & 0 deletions architecture/MIDDLEWARE.MD
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,28 @@ server := &http.Server{

1. Times the roundtrip latency for the response from the backend origin server and stores the latency in the context key `X-KAVA-PROXY-ORIGIN-ROUNDTRIP-LATENCY-MILLISECONDS` for use by other middleware.

#### Configuration

The proxy is configured to route requests based on their incoming Host. These are controlled via the
`PROXY_BACKEND_HOST_URL_MAP` environment variable.

As an example, consider a value of `localhost:7777>http://kava:8545,localhost:7778>http://kava:8545`.
This value is parsed into a map that looks like the following:
```
{
"localhost:7777" => "http://kava:8545",
"localhost:7778" => "http://kava:8545",
}
```
Any request to the service will be routed according to this map.
ie. all requests to local ports 7777 & 7778 get forwarded to `http://kava:8545`

Implementations of the [`Proxies` interface](../service/proxy.go#L13) contain logic for deciding
the backend host url to which a request is routed. This is used in the ProxyRequestMiddleware to
route requests.

Any request made to a host not in the map responds 502 Bad Gateway.

### After Proxy Middleware

1. Parses the request body and latency from context key values and creates a request metric for the proxied request.
15 changes: 3 additions & 12 deletions service/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"io"
"net/http"
"net/http/httputil"
"strings"
"time"

Expand Down Expand Up @@ -149,17 +148,9 @@ func createRequestLoggingMiddleware(h http.HandlerFunc, serviceLogger *logging.S
// through and executed before the response is written to the caller
func createProxyRequestMiddleware(next http.Handler, config config.Config, serviceLogger *logging.ServiceLogger, beforeRequestInterceptors []RequestInterceptor, afterRequestInterceptors []RequestInterceptor) http.HandlerFunc {
// create an http handler that will proxy any request to the specified URL
reverseProxyForHost := make(map[string]*httputil.ReverseProxy)
reverseProxyForHost := NewProxies(config, serviceLogger)

for host, proxyBackendURL := range config.ProxyBackendHostURLMapParsed {
serviceLogger.Debug().Msg(fmt.Sprintf("creating reverse proxy for host %s to %+v", host, proxyBackendURL))

targetURL := config.ProxyBackendHostURLMapParsed[host]

reverseProxyForHost[host] = httputil.NewSingleHostReverseProxy(&targetURL)
}

handler := func(proxies map[string]*httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
handler := func(proxies Proxies) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
serviceLogger.Trace().Msg(fmt.Sprintf("proxying request %+v", r))

Expand All @@ -176,7 +167,7 @@ func createProxyRequestMiddleware(next http.Handler, config config.Config, servi

// proxy the request to the backend origin server
// based on the request host
proxy, ok := proxies[r.Host]
proxy, ok := proxies.ProxyForRequest(r)

if !ok {
serviceLogger.Error().Msg(fmt.Sprintf("no matching proxy for host %s for request %+v\n configured proxies %+v", r.Host, r, proxies))
Expand Down
52 changes: 52 additions & 0 deletions service/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package service

import (
"fmt"
"net/http"
"net/http/httputil"

"github.com/kava-labs/kava-proxy-service/config"
"github.com/kava-labs/kava-proxy-service/logging"
)

// Proxies is an interface for getting a reverse proxy for a given request.
type Proxies interface {
ProxyForRequest(r *http.Request) (proxy *httputil.ReverseProxy, found bool)
}

// NewProxies creates a Proxies instance based on the service configuration:
// - for non-sharding configuration, it returns a HostProxies
// TODO: - for sharding configurations, it returns a HeightShardingProxies
func NewProxies(config config.Config, serviceLogger *logging.ServiceLogger) Proxies {
serviceLogger.Debug().Msg("configuring reverse proxies based solely on request host")
return newHostProxies(config, serviceLogger)
}

// HostProxies chooses a proxy based solely on the Host of the incoming request,
// and the host -> backend url map defined in the config.
type HostProxies struct {
proxyForHost map[string]*httputil.ReverseProxy
}

var _ Proxies = HostProxies{}

// ProxyForRequest implements Proxies. It determines the proxy based solely on the request Host.
func (hbp HostProxies) ProxyForRequest(r *http.Request) (*httputil.ReverseProxy, bool) {
proxy, found := hbp.proxyForHost[r.Host]
return proxy, found
}

// newHostProxies creates a HostProxies from the backend url map defined in the config.
func newHostProxies(config config.Config, serviceLogger *logging.ServiceLogger) HostProxies {
reverseProxyForHost := make(map[string]*httputil.ReverseProxy)

for host, proxyBackendURL := range config.ProxyBackendHostURLMapParsed {
serviceLogger.Debug().Msg(fmt.Sprintf("creating reverse proxy for host %s to %+v", host, proxyBackendURL))

targetURL := config.ProxyBackendHostURLMapParsed[host]

reverseProxyForHost[host] = httputil.NewSingleHostReverseProxy(&targetURL)
}

return HostProxies{proxyForHost: reverseProxyForHost}
}
80 changes: 80 additions & 0 deletions service/proxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package service_test

import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"testing"

"github.com/kava-labs/kava-proxy-service/config"
"github.com/kava-labs/kava-proxy-service/service"
"github.com/stretchr/testify/require"
)

func newConfig(t *testing.T, rawHostMap string) config.Config {
parsed, err := config.ParseRawProxyBackendHostURLMap(rawHostMap)
require.NoError(t, err)
return config.Config{
ProxyBackendHostURLMapRaw: rawHostMap,
ProxyBackendHostURLMapParsed: parsed,
}
}

func TestUnitTest_NewProxies(t *testing.T) {
t.Run("returns a HostProxies when sharding disabled", func(t *testing.T) {
config := newConfig(t, dummyConfig.ProxyBackendHostURLMapRaw)
proxies := service.NewProxies(config, dummyLogger)
require.IsType(t, service.HostProxies{}, proxies)
})
// TODO: HeightShardingProxies
}

func TestUnitTest_HostProxies(t *testing.T) {
config := newConfig(t,
"magic.kava.io>magicalbackend.kava.io,archive.kava.io>archivenode.kava.io,pruning.kava.io>pruningnode.kava.io",
)
proxies := service.NewProxies(config, dummyLogger)

t.Run("ProxyForHost maps to correct proxy", func(t *testing.T) {
req := mockReqForUrl("//magic.kava.io")
proxy, found := proxies.ProxyForRequest(req)
require.True(t, found, "expected proxy to be found")
requireProxyRoutesToUrl(t, proxy, req, "magicalbackend.kava.io/")

req = mockReqForUrl("https://archive.kava.io")
proxy, found = proxies.ProxyForRequest(req)
require.True(t, found, "expected proxy to be found")
requireProxyRoutesToUrl(t, proxy, req, "archivenode.kava.io/")

req = mockReqForUrl("//pruning.kava.io/some/nested/endpoint")
proxy, found = proxies.ProxyForRequest(req)
require.True(t, found, "expected proxy to be found")
requireProxyRoutesToUrl(t, proxy, req, "pruningnode.kava.io/some/nested/endpoint")
})

t.Run("ProxyForHost fails with unknown host", func(t *testing.T) {
_, found := proxies.ProxyForRequest(mockReqForUrl("//unknown-host.kava.io"))
require.False(t, found, "expected proxy not found for unknown host")
})
}

func mockReqForUrl(reqUrl string) *http.Request {
parsed, err := url.Parse(reqUrl)
if err != nil {
panic(fmt.Sprintf("unable to parse url %s: %s", reqUrl, err))
}
if parsed.Host == "" {
// absolute url is required for Host to be defined.
panic(fmt.Sprintf("test requires absolute url to determine host (prefix with '//' or 'https://'): found %s", reqUrl))
}
return &http.Request{Host: parsed.Host, URL: parsed}
}

// requireProxyRoutesToUrl is a test helper that verifies that
// the given proxy maps the provided request to the expected proxy backend
// relies on the fact that reverse proxies are given a Director that rewrite the request's URL
func requireProxyRoutesToUrl(t *testing.T, proxy *httputil.ReverseProxy, req *http.Request, expectedRoute string) {
proxy.Director(req)
require.Equal(t, expectedRoute, req.URL.String())
}

0 comments on commit 2f8fa7d

Please sign in to comment.