Initial QSfera import
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/proxy/pkg/config"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/qsfera/server/pkg/oidc"
|
||||
"github.com/qsfera/server/services/proxy/pkg/config"
|
||||
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMultipleSelectors in case there is more then one selector configured.
|
||||
ErrMultipleSelectors = fmt.Errorf("only one type of policy-selector (static, migration, claim or regex) can be configured")
|
||||
// ErrSelectorConfigIncomplete if policy_selector conf is missing
|
||||
ErrSelectorConfigIncomplete = fmt.Errorf("missing either \"static\", \"migration\", \"claim\" or \"regex\" configuration in policy_selector config ")
|
||||
// ErrUnexpectedConfigError unexpected config error
|
||||
ErrUnexpectedConfigError = fmt.Errorf("could not initialize policy-selector for given config")
|
||||
)
|
||||
|
||||
const (
|
||||
SelectorCookieName = "qsfera-selector"
|
||||
)
|
||||
|
||||
// Selector is a function which selects a proxy-policy based on the request.
|
||||
//
|
||||
// A policy is a random name which identifies a set of proxy-routes:
|
||||
//
|
||||
// {
|
||||
// "policies": [
|
||||
// {
|
||||
// "name": "us-east-1",
|
||||
// "routes": [
|
||||
// {
|
||||
// "endpoint": "/",
|
||||
// "backend": "https://backend.us.example.com:8080/app"
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// "name": "eu-ams-1",
|
||||
// "routes": [
|
||||
// {
|
||||
// "endpoint": "/",
|
||||
// "backend": "https://backend.eu.example.com:8080/app"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
type Selector func(r *http.Request) (string, error)
|
||||
|
||||
// LoadSelector constructs a specific policy-selector from a given configuration
|
||||
func LoadSelector(cfg *config.PolicySelector) (Selector, error) {
|
||||
selCount := 0
|
||||
|
||||
if cfg.Static != nil {
|
||||
selCount++
|
||||
}
|
||||
if cfg.Claims != nil {
|
||||
selCount++
|
||||
}
|
||||
if cfg.Regex != nil {
|
||||
selCount++
|
||||
}
|
||||
if selCount > 1 {
|
||||
return nil, ErrMultipleSelectors
|
||||
}
|
||||
|
||||
if cfg.Static == nil && cfg.Claims == nil && cfg.Regex == nil {
|
||||
return nil, ErrSelectorConfigIncomplete
|
||||
}
|
||||
|
||||
if cfg.Static != nil {
|
||||
return NewStaticSelector(cfg.Static), nil
|
||||
}
|
||||
|
||||
if cfg.Claims != nil {
|
||||
if cfg.Claims.SelectorCookieName == "" {
|
||||
cfg.Claims.SelectorCookieName = SelectorCookieName
|
||||
}
|
||||
return NewClaimsSelector(cfg.Claims), nil
|
||||
}
|
||||
|
||||
if cfg.Regex != nil {
|
||||
if cfg.Regex.SelectorCookieName == "" {
|
||||
cfg.Regex.SelectorCookieName = SelectorCookieName
|
||||
}
|
||||
return NewRegexSelector(cfg.Regex), nil
|
||||
}
|
||||
|
||||
return nil, ErrUnexpectedConfigError
|
||||
}
|
||||
|
||||
// NewStaticSelector returns a selector which uses a pre-configured policy.
|
||||
//
|
||||
// Configuration:
|
||||
//
|
||||
// "policy_selector": {
|
||||
// "static": {"policy" : "qsfera"}
|
||||
// },
|
||||
func NewStaticSelector(cfg *config.StaticSelectorConf) Selector {
|
||||
return func(r *http.Request) (s string, err error) {
|
||||
return cfg.Policy, nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewClaimsSelector selects the policy based on the "qsfera.routing.policy" claim
|
||||
// The policy for corner cases is configurable:
|
||||
//
|
||||
// "policy_selector": {
|
||||
// "migration": {
|
||||
// "default_policy" : "qsfera",
|
||||
// "unauthenticated_policy": "oc10"
|
||||
// }
|
||||
// },
|
||||
//
|
||||
// This selector can be used in migration-scenarios or to set up sharded deployments
|
||||
func NewClaimsSelector(cfg *config.ClaimsSelectorConf) Selector {
|
||||
return func(r *http.Request) (s string, err error) {
|
||||
|
||||
selectorCookie := func(r *http.Request) string {
|
||||
selectorCookie, err := r.Cookie(cfg.SelectorCookieName)
|
||||
if err == nil {
|
||||
// TODO check we know the routing policy?
|
||||
return selectorCookie.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// first, try to route by selector
|
||||
if claims := oidc.FromContext(r.Context()); claims != nil {
|
||||
if p, ok := claims[oidc.QsferaRoutingPolicy].(string); ok && p != "" {
|
||||
// TODO check we know the routing policy?
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// basic auth requests don't have a routing claim, so check for the cookie
|
||||
if s := selectorCookie(r); s != "" {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
return cfg.DefaultPolicy, nil
|
||||
}
|
||||
|
||||
// use cookie if provided
|
||||
if s := selectorCookie(r); s != "" {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
return cfg.UnauthenticatedPolicy, nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewRegexSelector selects the policy based on a user property
|
||||
// The policy for each case is configurable:
|
||||
//
|
||||
// "policy_selector": {
|
||||
// "regex": {
|
||||
// "matches_policies": [
|
||||
// {"priority": 10, "property": "mail", "match": "mary@example.org", "policy": "qsfera"},
|
||||
// {"priority": 20, "property": "mail", "match": "[^@]+@example.org", "policy": "oc10"},
|
||||
// {"priority": 30, "property": "username", "match": "(dennis|feynman)", "policy": "qsfera"},
|
||||
// {"priority": 40, "property": "username", "match": ".+", "policy": "oc10"},
|
||||
// {"priority": 50, "property": "id", "match": "b1f74ec4-dd7e-11ef-a543-03775734d0f7", "policy": "qsfera"},
|
||||
// {"priority": 60, "property": "id", "match": "056fc874-dd7f-11ef-ba84-af6fca4b7289", "policy": "oc10"}
|
||||
// ],
|
||||
// "unauthenticated_policy": "oc10"
|
||||
// }
|
||||
// },
|
||||
//
|
||||
// This selector can be used in migration-scenarios or to set up sharded deployments
|
||||
func NewRegexSelector(cfg *config.RegexSelectorConf) Selector {
|
||||
regexRules := []*regexRule{}
|
||||
sort.Slice(cfg.MatchesPolicies, func(i, j int) bool {
|
||||
return cfg.MatchesPolicies[i].Priority < cfg.MatchesPolicies[j].Priority
|
||||
})
|
||||
for i := range cfg.MatchesPolicies {
|
||||
regexRules = append(regexRules, ®exRule{
|
||||
property: cfg.MatchesPolicies[i].Property,
|
||||
rule: regexp.MustCompile(cfg.MatchesPolicies[i].Match),
|
||||
policy: cfg.MatchesPolicies[i].Policy,
|
||||
})
|
||||
}
|
||||
return func(r *http.Request) (s string, err error) {
|
||||
// use cookie first if provided
|
||||
selectorCookie, err := r.Cookie(cfg.SelectorCookieName)
|
||||
if err == nil {
|
||||
return selectorCookie.Value, nil
|
||||
}
|
||||
|
||||
// if no cookie is present, try to route by selector
|
||||
if u, ok := revactx.ContextGetUser(r.Context()); ok {
|
||||
for i := range regexRules {
|
||||
switch regexRules[i].property {
|
||||
case "mail":
|
||||
if regexRules[i].rule.MatchString(u.Mail) {
|
||||
return regexRules[i].policy, nil
|
||||
}
|
||||
case "username":
|
||||
if regexRules[i].rule.MatchString(u.Username) {
|
||||
return regexRules[i].policy, nil
|
||||
}
|
||||
case "id":
|
||||
if u.Id != nil && regexRules[i].rule.MatchString(u.Id.OpaqueId) {
|
||||
return regexRules[i].policy, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return cfg.DefaultPolicy, nil
|
||||
}
|
||||
|
||||
return cfg.UnauthenticatedPolicy, nil
|
||||
}
|
||||
}
|
||||
|
||||
type regexRule struct {
|
||||
property string
|
||||
rule *regexp.Regexp
|
||||
policy string
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/qsfera/server/pkg/oidc"
|
||||
"github.com/qsfera/server/services/proxy/pkg/config"
|
||||
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
)
|
||||
|
||||
func TestLoadSelector(t *testing.T) {
|
||||
type test struct {
|
||||
cfg *config.PolicySelector
|
||||
expectedErr error
|
||||
}
|
||||
sCfg := &config.StaticSelectorConf{Policy: "reva"}
|
||||
ccfg := &config.ClaimsSelectorConf{}
|
||||
rcfg := &config.RegexSelectorConf{}
|
||||
|
||||
table := []test{
|
||||
{cfg: &config.PolicySelector{Static: sCfg, Claims: ccfg, Regex: rcfg}, expectedErr: ErrMultipleSelectors},
|
||||
{cfg: &config.PolicySelector{}, expectedErr: ErrSelectorConfigIncomplete},
|
||||
{cfg: &config.PolicySelector{Static: sCfg}, expectedErr: nil},
|
||||
{cfg: &config.PolicySelector{Claims: ccfg}, expectedErr: nil},
|
||||
{cfg: &config.PolicySelector{Regex: rcfg}, expectedErr: nil},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
_, err := LoadSelector(test.cfg)
|
||||
if err != test.expectedErr {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticSelector(t *testing.T) {
|
||||
sel := NewStaticSelector(&config.StaticSelectorConf{Policy: "qsfera"})
|
||||
req := httptest.NewRequest("GET", "https://example.org/foo", nil)
|
||||
want := "qsfera"
|
||||
got, err := sel(req)
|
||||
if got != want {
|
||||
t.Errorf("Expected policy %v got %v", want, got)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
sel = NewStaticSelector(&config.StaticSelectorConf{Policy: "foo"})
|
||||
|
||||
want = "foo"
|
||||
got, err = sel(req)
|
||||
if got != want {
|
||||
t.Errorf("Expected policy %v got %v", want, got)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
Name string
|
||||
Context context.Context
|
||||
Cookie *http.Cookie
|
||||
Expected string
|
||||
}
|
||||
|
||||
func TestClaimsSelector(t *testing.T) {
|
||||
sel := NewClaimsSelector(&config.ClaimsSelectorConf{
|
||||
DefaultPolicy: "default",
|
||||
UnauthenticatedPolicy: "unauthenticated",
|
||||
SelectorCookieName: SelectorCookieName,
|
||||
})
|
||||
|
||||
var tests = []testCase{
|
||||
{"unauthenticated", context.Background(), nil, "unauthenticated"},
|
||||
{"default", oidc.NewContext(context.Background(), map[string]any{oidc.QsferaRoutingPolicy: ""}), nil, "default"},
|
||||
{"claim-value", oidc.NewContext(context.Background(), map[string]any{oidc.QsferaRoutingPolicy: "qsfera.routing.policy-value"}), nil, "qsfera.routing.policy-value"},
|
||||
{"cookie-only", context.Background(), &http.Cookie{Name: SelectorCookieName, Value: "cookie"}, "cookie"},
|
||||
{"claim-can-override-cookie", oidc.NewContext(context.Background(), map[string]any{oidc.QsferaRoutingPolicy: "qsfera.routing.policy-value"}), &http.Cookie{Name: SelectorCookieName, Value: "cookie"}, "qsfera.routing.policy-value"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
r := httptest.NewRequest("GET", "https://example.com", nil)
|
||||
if tc.Cookie != nil {
|
||||
r.AddCookie(tc.Cookie)
|
||||
}
|
||||
nr := r.WithContext(tc.Context)
|
||||
got, err := sel(nr)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if got != tc.Expected {
|
||||
t.Errorf("Expected Policy %v got %v", tc.Expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexSelector(t *testing.T) {
|
||||
sel := NewRegexSelector(&config.RegexSelectorConf{
|
||||
DefaultPolicy: "default",
|
||||
MatchesPolicies: []config.RegexRuleConf{
|
||||
{Priority: 10, Property: "mail", Match: "mary@example.org", Policy: "qsfera"},
|
||||
{Priority: 20, Property: "mail", Match: "[^@]+@example.org", Policy: "oc10"},
|
||||
{Priority: 30, Property: "username", Match: "(alan|feynman)", Policy: "qsfera"},
|
||||
{Priority: 40, Property: "username", Match: ".+", Policy: "oc10"},
|
||||
{Priority: 50, Property: "id", Match: "b1f74ec4-dd7e-11ef-a543-03775734d0f7", Policy: "qsfera"},
|
||||
{Priority: 60, Property: "id", Match: "056fc874-dd7f-11ef-ba84-af6fca4b7289", Policy: "oc10"},
|
||||
},
|
||||
UnauthenticatedPolicy: "unauthenticated",
|
||||
})
|
||||
|
||||
var tests = []testCase{
|
||||
{"unauthenticated", context.Background(), nil, "unauthenticated"},
|
||||
{"default", revactx.ContextSetUser(context.Background(), &userv1beta1.User{}), nil, "default"},
|
||||
{"mail-qsfera", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Mail: "mary@example.org"}), nil, "qsfera"},
|
||||
{"mail-oc10", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Mail: "alan@example.org"}), nil, "oc10"},
|
||||
{"username-alan", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Username: "alan"}), nil, "qsfera"},
|
||||
{"username-feynman", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Username: "feynman"}), nil, "qsfera"},
|
||||
{"username-mary", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Username: "mary"}), nil, "oc10"},
|
||||
{"id-nil", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Id: &userv1beta1.UserId{}}), nil, "default"},
|
||||
{"id-1", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Id: &userv1beta1.UserId{OpaqueId: "b1f74ec4-dd7e-11ef-a543-03775734d0f7"}}), nil, "qsfera"},
|
||||
{"id-2", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Id: &userv1beta1.UserId{OpaqueId: "056fc874-dd7f-11ef-ba84-af6fca4b7289"}}), nil, "oc10"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
r := httptest.NewRequest("GET", "https://example.com", nil)
|
||||
nr := r.WithContext(tc.Context)
|
||||
got, err := sel(nr)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if got != tc.Expected {
|
||||
t.Errorf("Expected Policy %v got %v", tc.Expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
stdlog "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/proxy/pkg/config"
|
||||
"github.com/qsfera/server/services/proxy/pkg/proxy/policy"
|
||||
"github.com/qsfera/server/services/proxy/pkg/router"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// MultiHostReverseProxy extends "httputil" to support multiple hosts with different policies
|
||||
type MultiHostReverseProxy struct {
|
||||
httputil.ReverseProxy
|
||||
// Directors holds policy route type method endpoint Director
|
||||
Directors map[string]map[config.RouteType]map[string]map[string]func(req *http.Request)
|
||||
PolicySelector policy.Selector
|
||||
logger log.Logger
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewMultiHostReverseProxy creates a new MultiHostReverseProxy
|
||||
func NewMultiHostReverseProxy(opts ...Option) (*MultiHostReverseProxy, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
rp := &MultiHostReverseProxy{
|
||||
ReverseProxy: httputil.ReverseProxy{
|
||||
ErrorLog: stdlog.New(options.Logger, "", 0),
|
||||
},
|
||||
Directors: make(map[string]map[config.RouteType]map[string]map[string]func(req *http.Request)),
|
||||
logger: options.Logger,
|
||||
config: options.Config,
|
||||
}
|
||||
|
||||
rp.Rewrite = func(r *httputil.ProxyRequest) {
|
||||
ri := router.ContextRoutingInfo(r.In.Context())
|
||||
ri.Rewrite()(r)
|
||||
}
|
||||
|
||||
rp.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) {
|
||||
reqLogger := zerolog.Ctx(req.Context())
|
||||
if ev := reqLogger.Error(); ev.Enabled() {
|
||||
ev.Err(err).Msg("error happened in MultiHostReverseProxy")
|
||||
} else {
|
||||
rp.logger.Err(err).Msg("error happened in MultiHostReverseProxy")
|
||||
}
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
}
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: options.Config.InsecureBackends, //nolint:gosec
|
||||
}
|
||||
if options.Config.BackendHTTPSCACert != "" {
|
||||
certs := x509.NewCertPool()
|
||||
pemData, err := os.ReadFile(options.Config.BackendHTTPSCACert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !certs.AppendCertsFromPEM(pemData) {
|
||||
return nil, errors.New("Error initializing LDAP Backend. Adding CA cert failed")
|
||||
}
|
||||
tlsConf.RootCAs = certs
|
||||
}
|
||||
// equals http.DefaultTransport except TLSClientConfig
|
||||
rp.Transport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: tlsConf,
|
||||
}
|
||||
return rp, nil
|
||||
}
|
||||
|
||||
func (p *MultiHostReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
p.ReverseProxy.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/pkg/registry"
|
||||
"github.com/qsfera/server/services/proxy/pkg/config"
|
||||
"github.com/qsfera/server/services/proxy/pkg/router"
|
||||
"go-micro.dev/v4/selector"
|
||||
)
|
||||
|
||||
func TestProxyIntegration(t *testing.T) {
|
||||
var tests = []testCase{
|
||||
// Simple prefix route
|
||||
test("simple_prefix", withPolicy("qsfera", withRoutes{{
|
||||
Type: config.PrefixRoute,
|
||||
Endpoint: "/api",
|
||||
Backend: "http://api.example.com"},
|
||||
})).withRequest("GET", "https://example.com/api", nil).
|
||||
expectProxyTo("http://api.example.com/api"),
|
||||
|
||||
// Complex prefix route, different method
|
||||
test("complex_prefix_post", withPolicy("qsfera", withRoutes{{
|
||||
Type: config.PrefixRoute,
|
||||
Endpoint: "/api",
|
||||
Backend: "http://api.example.com/service1/"},
|
||||
})).withRequest("POST", "https://example.com/api", nil).
|
||||
expectProxyTo("http://api.example.com/service1/api"),
|
||||
|
||||
// Query route
|
||||
test("query_route", withPolicy("qsfera", withRoutes{{
|
||||
Type: config.QueryRoute,
|
||||
Endpoint: "/api?format=json",
|
||||
Backend: "http://backend/"},
|
||||
})).withRequest("GET", "https://example.com/api?format=json", nil).
|
||||
expectProxyTo("http://backend/api?format=json"),
|
||||
|
||||
// Regex route
|
||||
test("regex_route", withPolicy("qsfera", withRoutes{{
|
||||
Type: config.RegexRoute,
|
||||
Endpoint: `\/user\/(\d+)`,
|
||||
Backend: "http://backend/"},
|
||||
})).withRequest("POST", "https://example.com/user/1234", nil).
|
||||
expectProxyTo("http://backend/user/1234"),
|
||||
|
||||
// Multiple prefix routes 1
|
||||
test("multiple_prefix", withPolicy("qsfera", withRoutes{
|
||||
{
|
||||
Type: config.PrefixRoute,
|
||||
Endpoint: "/api",
|
||||
Backend: "http://api.example.com",
|
||||
},
|
||||
{
|
||||
Type: config.PrefixRoute,
|
||||
Endpoint: "/payment",
|
||||
Backend: "http://payment.example.com",
|
||||
},
|
||||
})).withRequest("GET", "https://example.com/payment", nil).
|
||||
expectProxyTo("http://payment.example.com/payment"),
|
||||
|
||||
// Multiple prefix routes 2
|
||||
test("multiple_prefix", withPolicy("qsfera", withRoutes{
|
||||
{
|
||||
Type: config.PrefixRoute,
|
||||
Endpoint: "/api",
|
||||
Backend: "http://api.example.com",
|
||||
},
|
||||
{
|
||||
Type: config.PrefixRoute,
|
||||
Endpoint: "/payment",
|
||||
Backend: "http://payment.example.com",
|
||||
},
|
||||
})).withRequest("GET", "https://example.com/api", nil).
|
||||
expectProxyTo("http://api.example.com/api"),
|
||||
|
||||
// Mixed route types
|
||||
test("mixed_types", withPolicy("qsfera", withRoutes{
|
||||
{
|
||||
Type: config.PrefixRoute,
|
||||
Endpoint: "/api",
|
||||
Backend: "http://api.example.com",
|
||||
},
|
||||
{
|
||||
Type: config.RegexRoute,
|
||||
Endpoint: `\/user\/(\d+)`,
|
||||
Backend: "http://users.example.com",
|
||||
ApacheVHost: false,
|
||||
},
|
||||
})).withRequest("GET", "https://example.com/api", nil).
|
||||
expectProxyTo("http://api.example.com/api"),
|
||||
|
||||
// Mixed route types
|
||||
test("mixed_types", withPolicy("qsfera", withRoutes{
|
||||
{
|
||||
Type: config.PrefixRoute,
|
||||
Endpoint: "/api",
|
||||
Backend: "http://api.example.com",
|
||||
},
|
||||
{
|
||||
Type: config.RegexRoute,
|
||||
Endpoint: `\/user\/(\d+)`,
|
||||
Backend: "http://users.example.com",
|
||||
ApacheVHost: false,
|
||||
},
|
||||
})).withRequest("GET", "https://example.com/user/1234", nil).
|
||||
expectProxyTo("http://users.example.com/user/1234"),
|
||||
}
|
||||
|
||||
reg := registry.GetRegistry()
|
||||
sel := selector.NewSelector(selector.Registry(reg))
|
||||
|
||||
for k := range tests {
|
||||
t.Run(tests[k].id, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tc := tests[k]
|
||||
|
||||
rt := router.Middleware(sel, nil, tc.conf, log.NewLogger())
|
||||
rp := newTestProxy(testConfig(tc.conf), func(req *http.Request) *http.Response {
|
||||
if got, want := req.URL.String(), tc.expect.String(); got != want {
|
||||
t.Errorf("Proxied url should be %v got %v", want, got)
|
||||
}
|
||||
|
||||
if got, want := req.Method, tc.input.Method; got != want {
|
||||
t.Errorf("Proxied request method should be %v got %v", want, got)
|
||||
}
|
||||
|
||||
if got, want := req.Proto, tc.input.Proto; got != want {
|
||||
t.Errorf("Proxied request proto should be %v got %v", want, got)
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`OK`)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
})
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
rt(rp).ServeHTTP(rr, tc.input)
|
||||
rsp := rr.Result()
|
||||
|
||||
if rsp.StatusCode != 200 {
|
||||
t.Errorf("Expected status 200 from proxy-response got %v", rsp.StatusCode)
|
||||
}
|
||||
|
||||
resultBody, err := io.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
t.Fatal("Error reading result body")
|
||||
}
|
||||
if err = rsp.Body.Close(); err != nil {
|
||||
t.Fatal("Error closing result body")
|
||||
}
|
||||
|
||||
bodyString := string(resultBody)
|
||||
if bodyString != `OK` {
|
||||
t.Errorf("Result body of proxied response should be OK, got %v", bodyString)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newTestProxy(cfg *config.Config, fn RoundTripFunc) *MultiHostReverseProxy {
|
||||
rp, _ := NewMultiHostReverseProxy(Config(cfg))
|
||||
rp.Transport = fn
|
||||
return rp
|
||||
}
|
||||
|
||||
type RoundTripFunc func(req *http.Request) *http.Response
|
||||
|
||||
// RoundTrip .
|
||||
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req), nil
|
||||
}
|
||||
|
||||
type withRoutes []config.Route
|
||||
|
||||
type testCase struct {
|
||||
id string
|
||||
input *http.Request
|
||||
expect *url.URL
|
||||
conf []config.Policy
|
||||
}
|
||||
|
||||
func test(id string, policies ...config.Policy) *testCase {
|
||||
tc := &testCase{
|
||||
id: id,
|
||||
}
|
||||
for k := range policies {
|
||||
tc.conf = append(tc.conf, policies[k])
|
||||
}
|
||||
|
||||
return tc
|
||||
}
|
||||
|
||||
func withPolicy(name string, r withRoutes) config.Policy {
|
||||
return config.Policy{Name: name, Routes: r}
|
||||
}
|
||||
|
||||
func (tc *testCase) withRequest(method string, target string, body io.Reader) *testCase {
|
||||
tc.input = httptest.NewRequest(method, target, body)
|
||||
return tc
|
||||
}
|
||||
|
||||
func (tc *testCase) expectProxyTo(strURL string) testCase {
|
||||
pu, err := url.Parse(strURL)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error parsing %v", strURL))
|
||||
}
|
||||
|
||||
tc.expect = pu
|
||||
return *tc
|
||||
}
|
||||
|
||||
func testConfig(policy []config.Policy) *config.Config {
|
||||
return &config.Config{
|
||||
Debug: config.Debug{},
|
||||
HTTP: config.HTTP{},
|
||||
Policies: policy,
|
||||
OIDC: config.OIDC{},
|
||||
PolicySelector: nil,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user