rename folder extensions -> services

Signed-off-by: Christian Richter <crichter@owncloud.com>
This commit is contained in:
Christian Richter
2022-06-27 14:05:36 +02:00
parent 1aea93d8cd
commit 78064e6bab
926 changed files with 0 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
package proxy
import (
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
)
// 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
}
}
+220
View File
@@ -0,0 +1,220 @@
package policy
import (
"fmt"
"net/http"
"regexp"
"sort"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
)
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 = "owncloud-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" : "ocis"}
// },
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 "ocis.routing.policy" claim
// The policy for corner cases is configurable:
// "policy_selector": {
// "migration": {
// "default_policy" : "ocis",
// "unauthenticated_policy": "oc10"
// }
// },
//
// This selector can be used in migration-scenarios where some users have already migrated from ownCloud10 to OCIS and
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.OcisRoutingPolicy].(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": "marie@example.org", "policy": "ocis"},
// {"priority": 20, "property": "mail", "match": "[^@]+@example.org", "policy": "oc10"},
// {"priority": 30, "property": "username", "match": "(einstein|feynman)", "policy": "ocis"},
// {"priority": 40, "property": "username", "match": ".+", "policy": "oc10"},
// {"priority": 50, "property": "id", "match": "4c510ada-c86b-4815-8820-42cdf82c3d51", "policy": "ocis"},
// {"priority": 60, "property": "id", "match": "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", "policy": "oc10"}
// ],
// "unauthenticated_policy": "oc10"
// }
// },
//
// This selector can be used in migration-scenarios where some users have already migrated from ownCloud10 to OCIS and
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, &regexRule{
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"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/config"
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
)
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: "ocis"})
req := httptest.NewRequest("GET", "https://example.org/foo", nil)
want := "ocis"
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",
})
var tests = []testCase{
{"unauthenticated", context.Background(), nil, "unauthenticated"},
{"default", oidc.NewContext(context.Background(), map[string]interface{}{oidc.OcisRoutingPolicy: ""}), nil, "default"},
{"claim-value", oidc.NewContext(context.Background(), map[string]interface{}{oidc.OcisRoutingPolicy: "ocis.routing.policy-value"}), nil, "ocis.routing.policy-value"},
{"cookie-only", context.Background(), &http.Cookie{Name: SelectorCookieName, Value: "cookie"}, "cookie"},
{"claim-can-override-cookie", oidc.NewContext(context.Background(), map[string]interface{}{oidc.OcisRoutingPolicy: "ocis.routing.policy-value"}), &http.Cookie{Name: SelectorCookieName, Value: "cookie"}, "ocis.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: "marie@example.org", Policy: "ocis"},
{Priority: 20, Property: "mail", Match: "[^@]+@example.org", Policy: "oc10"},
{Priority: 30, Property: "username", Match: "(einstein|feynman)", Policy: "ocis"},
{Priority: 40, Property: "username", Match: ".+", Policy: "oc10"},
{Priority: 50, Property: "id", Match: "4c510ada-c86b-4815-8820-42cdf82c3d51", Policy: "ocis"},
{Priority: 60, Property: "id", Match: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", Policy: "oc10"},
},
UnauthenticatedPolicy: "unauthenticated",
})
var tests = []testCase{
{"unauthenticated", context.Background(), nil, "unauthenticated"},
{"default", revactx.ContextSetUser(context.Background(), &userv1beta1.User{}), nil, "default"},
{"mail-ocis", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Mail: "marie@example.org"}), nil, "ocis"},
{"mail-oc10", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Mail: "einstein@example.org"}), nil, "oc10"},
{"username-einstein", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Username: "einstein"}), nil, "ocis"},
{"username-feynman", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Username: "feynman"}), nil, "ocis"},
{"username-marie", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Username: "marie"}), 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: "4c510ada-c86b-4815-8820-42cdf82c3d51"}}), nil, "ocis"},
{"id-2", revactx.ContextSetUser(context.Background(), &userv1beta1.User{Id: &userv1beta1.UserId{OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c"}}), nil, "oc10"},
}
for _, tc := range tests {
tc := tc // capture range variable
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)
}
})
}
}
+303
View File
@@ -0,0 +1,303 @@
package proxy
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"regexp"
"strings"
"time"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"go-micro.dev/v4/selector"
"go.opentelemetry.io/otel/attribute"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/config"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/proxy/policy"
proxytracing "github.com/owncloud/ocis/v2/extensions/proxy/pkg/tracing"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
pkgtrace "github.com/owncloud/ocis/v2/ocis-pkg/tracing"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
// 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 {
options := newOptions(opts...)
rp := &MultiHostReverseProxy{
Directors: make(map[string]map[config.RouteType]map[string]map[string]func(req *http.Request)),
logger: options.Logger,
config: options.Config,
}
rp.Director = rp.directorSelectionDirector
// 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: &tls.Config{
InsecureSkipVerify: options.Config.InsecureBackends, //nolint:gosec
},
}
if options.Config.PolicySelector == nil {
firstPolicy := options.Config.Policies[0].Name
rp.logger.Warn().Str("policy", firstPolicy).Msg("policy-selector not configured. Will always use first policy")
options.Config.PolicySelector = &config.PolicySelector{
Static: &config.StaticSelectorConf{
Policy: firstPolicy,
},
}
}
rp.logger.Debug().
Interface("selector_config", options.Config.PolicySelector).
Msg("loading policy-selector")
policySelector, err := policy.LoadSelector(options.Config.PolicySelector)
if err != nil {
rp.logger.Fatal().Err(err).Msg("Could not load policy-selector")
}
rp.PolicySelector = policySelector
for _, pol := range options.Config.Policies {
for _, route := range pol.Routes {
rp.logger.Debug().Str("fwd: ", route.Endpoint)
if route.Backend == "" && route.Service == "" {
rp.logger.Fatal().Interface("route", route).Msg("neither Backend nor Service is set")
}
uri, err2 := url.Parse(route.Backend)
if err2 != nil {
rp.logger.
Fatal(). // fail early on misconfiguration
Err(err2).
Str("backend", route.Backend).
Msg("malformed url")
}
// here the backend is used as a uri
rp.AddHost(pol.Name, uri, route)
}
}
return rp
}
func (p *MultiHostReverseProxy) directorSelectionDirector(r *http.Request) {
pol, err := p.PolicySelector(r)
if err != nil {
p.logger.Error().Err(err).Msg("Error while selecting pol")
return
}
if _, ok := p.Directors[pol]; !ok {
p.logger.
Error().
Str("policy", pol).
Msg("policy is not configured")
return
}
method := ""
// find matching director
for _, rt := range config.RouteTypes {
var handler func(string, url.URL) bool
switch rt {
case config.QueryRoute:
handler = p.queryRouteMatcher
case config.RegexRoute:
handler = p.regexRouteMatcher
case config.PrefixRoute:
fallthrough
default:
handler = p.prefixRouteMatcher
}
if p.Directors[pol][rt][r.Method] != nil {
// use specific method
method = r.Method
}
for endpoint := range p.Directors[pol][rt][method] {
if handler(endpoint, *r.URL) {
p.logger.Debug().
Str("policy", pol).
Str("method", r.Method).
Str("prefix", endpoint).
Str("path", r.URL.Path).
Str("routeType", string(rt)).
Msg("director found")
p.Directors[pol][rt][method][endpoint](r)
return
}
}
}
// override default director with root. If any
switch {
case p.Directors[pol][config.PrefixRoute][method]["/"] != nil:
// try specific method
p.Directors[pol][config.PrefixRoute][method]["/"](r)
return
case p.Directors[pol][config.PrefixRoute][""]["/"] != nil:
// fallback to unspecific method
p.Directors[pol][config.PrefixRoute][""]["/"](r)
return
}
p.logger.
Warn().
Str("policy", pol).
Str("path", r.URL.Path).
Msg("no director found")
}
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
// AddHost undocumented
func (p *MultiHostReverseProxy) AddHost(policy string, target *url.URL, rt config.Route) {
targetQuery := target.RawQuery
if p.Directors[policy] == nil {
p.Directors[policy] = make(map[config.RouteType]map[string]map[string]func(req *http.Request))
}
routeType := config.DefaultRouteType
if rt.Type != "" {
routeType = rt.Type
}
if p.Directors[policy][routeType] == nil {
p.Directors[policy][routeType] = make(map[string]map[string]func(req *http.Request))
}
if p.Directors[policy][routeType][rt.Method] == nil {
p.Directors[policy][routeType][rt.Method] = make(map[string]func(req *http.Request))
}
reg := registry.GetRegistry()
sel := selector.NewSelector(selector.Registry(reg))
p.Directors[policy][routeType][rt.Method][rt.Endpoint] = func(req *http.Request) {
if rt.Service != "" {
// select next node
next, err := sel.Select(rt.Service)
if err != nil {
fmt.Println(fmt.Errorf("could not select %s service from the registry: %v", rt.Service, err))
return // TODO error? fallback to target.Host & Scheme?
}
node, err := next()
if err != nil {
fmt.Println(fmt.Errorf("could not select next node for service %s: %v", rt.Service, err))
return // TODO error? fallback to target.Host & Scheme?
}
req.URL.Host = node.Address
req.URL.Scheme = node.Metadata["protocol"] // TODO check property exists?
} else {
req.URL.Host = target.Host
req.URL.Scheme = target.Scheme
}
// Apache deployments host addresses need to match on req.Host and req.URL.Host
// see https://stackoverflow.com/questions/34745654/golang-reverseproxy-with-apache2-sni-hostname-error
if rt.ApacheVHost {
req.Host = target.Host
}
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
// explicitly disable User-Agent so it's not set to default value
req.Header.Set("User-Agent", "")
}
}
}
func (p *MultiHostReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
span trace.Span
)
tracer := proxytracing.TraceProvider.Tracer("proxy")
ctx, span = tracer.Start(ctx, fmt.Sprintf("%s %v", r.Method, r.URL.Path))
defer span.End()
span.SetAttributes(
attribute.KeyValue{
Key: "x-request-id",
Value: attribute.StringValue(chimiddleware.GetReqID(r.Context())),
})
pkgtrace.Propagator.Inject(ctx, propagation.HeaderCarrier(r.Header))
p.ReverseProxy.ServeHTTP(w, r.WithContext(ctx))
}
func (p MultiHostReverseProxy) queryRouteMatcher(endpoint string, target url.URL) bool {
u, _ := url.Parse(endpoint)
if !strings.HasPrefix(target.Path, u.Path) || endpoint == "/" {
return false
}
q := u.Query()
if len(q) == 0 {
return false
}
tq := target.Query()
for k := range q {
if q.Get(k) != tq.Get(k) {
return false
}
}
return true
}
func (p *MultiHostReverseProxy) regexRouteMatcher(pattern string, target url.URL) bool {
matched, err := regexp.MatchString(pattern, target.String())
if err != nil {
p.logger.Warn().Err(err).Str("pattern", pattern).Msg("regex with pattern failed")
}
return matched
}
func (p *MultiHostReverseProxy) prefixRouteMatcher(prefix string, target url.URL) bool {
return strings.HasPrefix(target.Path, prefix) && prefix != "/"
}
@@ -0,0 +1,224 @@
package proxy
import (
"bytes"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/config"
)
func TestProxyIntegration(t *testing.T) {
var tests = []testCase{
// Simple prefix route
test("simple_prefix", withPolicy("ocis", 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("ocis", 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("ocis", 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("ocis", 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("ocis", 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("ocis", 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("ocis", 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("ocis", 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"),
}
for k := range tests {
t.Run(tests[k].id, func(t *testing.T) {
t.Parallel()
tc := tests[k]
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: ioutil.NopCloser(bytes.NewBufferString(`OK`)),
Header: make(http.Header),
}
})
rr := httptest.NewRecorder()
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 := ioutil.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 {
log.Fatalf("Error parsing %v", strURL)
}
tc.expect = pu
return *tc
}
func testConfig(policy []config.Policy) *config.Config {
return &config.Config{
Log: &config.Log{},
Debug: config.Debug{},
HTTP: config.HTTP{},
Tracing: &config.Tracing{},
Policies: policy,
OIDC: config.OIDC{},
PolicySelector: nil,
}
}
+137
View File
@@ -0,0 +1,137 @@
package proxy
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/config"
"github.com/owncloud/ocis/v2/extensions/proxy/pkg/config/defaults"
)
type matchertest struct {
method, endpoint, target string
matches bool
}
func TestPrefixRouteMatcher(t *testing.T) {
cfg := defaults.DefaultConfig()
cfg.Policies = defaults.DefaultPolicies()
p := NewMultiHostReverseProxy(Config(cfg))
table := []matchertest{
{endpoint: "/foobar", target: "/foobar/baz/some/url", matches: true},
{endpoint: "/fobar", target: "/foobar/baz/some/url", matches: false},
}
for _, test := range table {
u, _ := url.Parse(test.target)
matched := p.prefixRouteMatcher(test.endpoint, *u)
if matched != test.matches {
t.Errorf("PrefixRouteMatcher returned %t expected %t for endpoint: %s and target %s",
matched, test.matches, test.endpoint, u.String())
}
}
}
func TestQueryRouteMatcher(t *testing.T) {
cfg := defaults.DefaultConfig()
cfg.Policies = defaults.DefaultPolicies()
p := NewMultiHostReverseProxy(Config(cfg))
table := []matchertest{
{endpoint: "/foobar?parameter=true", target: "/foobar/baz/some/url?parameter=true", matches: true},
{endpoint: "/foobar", target: "/foobar/baz/some/url?parameter=true", matches: false},
{endpoint: "/foobar?parameter=false", target: "/foobar/baz/some/url?parameter=true", matches: false},
{endpoint: "/foobar?parameter=false&other=true", target: "/foobar/baz/some/url?parameter=true", matches: false},
{
endpoint: "/foobar?parameter=false&other=true",
target: "/foobar/baz/some/url?parameter=false&other=true",
matches: true,
},
{endpoint: "/fobar", target: "/foobar", matches: false},
}
for _, test := range table {
u, _ := url.Parse(test.target)
matched := p.queryRouteMatcher(test.endpoint, *u)
if matched != test.matches {
t.Errorf("QueryRouteMatcher returned %t expected %t for endpoint: %s and target %s",
matched, test.matches, test.endpoint, u.String())
}
}
}
func TestRegexRouteMatcher(t *testing.T) {
cfg := defaults.DefaultConfig()
cfg.Policies = defaults.DefaultPolicies()
p := NewMultiHostReverseProxy(Config(cfg))
table := []matchertest{
{endpoint: ".*some\\/url.*parameter=true", target: "/foobar/baz/some/url?parameter=true", matches: true},
{endpoint: "([\\])\\w+", target: "/foobar/baz/some/url?parameter=true", matches: false},
}
for _, test := range table {
u, _ := url.Parse(test.target)
matched := p.regexRouteMatcher(test.endpoint, *u)
if matched != test.matches {
t.Errorf("RegexRouteMatcher returned %t expected %t for endpoint: %s and target %s",
matched, test.matches, test.endpoint, u.String())
}
}
}
func TestSingleJoiningSlash(t *testing.T) {
type test struct {
a, b, result string
}
table := []test{
{a: "a", b: "b", result: "a/b"},
{a: "a/", b: "b", result: "a/b"},
{a: "a", b: "/b", result: "a/b"},
{a: "a/", b: "/b", result: "a/b"},
}
for _, test := range table {
p := singleJoiningSlash(test.a, test.b)
if p != test.result {
t.Errorf("SingleJoiningSlash got %s expected %s", p, test.result)
}
}
}
func TestDirectorSelectionDirector(t *testing.T) {
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ok")
}))
defer svr.Close()
p := NewMultiHostReverseProxy(Config(&config.Config{
PolicySelector: &config.PolicySelector{
Static: &config.StaticSelectorConf{
Policy: "default",
},
},
}))
p.AddHost("default", &url.URL{Host: "ocdav"}, config.Route{Type: config.PrefixRoute, Method: "", Endpoint: "/dav", Backend: "ocdav"})
p.AddHost("default", &url.URL{Host: "ocis-webdav"}, config.Route{Type: config.PrefixRoute, Method: "REPORT", Endpoint: "/dav", Backend: "ocis-webdav"})
table := []matchertest{
{method: "PROPFIND", endpoint: "/dav/files/demo/", target: "ocdav"},
{method: "REPORT", endpoint: "/dav/files/demo/", target: "ocis-webdav"},
}
for _, test := range table {
r := httptest.NewRequest(test.method, "/dav/files/demo/", nil)
p.directorSelectionDirector(r)
if r.URL.Host != test.target {
t.Errorf("TestDirectorSelectionDirector got host %s expected %s", r.Host, test.target)
}
}
}