From 8329a8ea9c4075d3a11db2cc68a3502f6d05ca49 Mon Sep 17 00:00:00 2001 From: yusing Date: Thu, 3 Oct 2024 01:50:49 +0800 Subject: [PATCH] replacing label parser map with improved deserialization implementation, API host check now disabled when in debug mode --- cmd/main.go | 4 - docs/docker.md | 39 +++++- docs/middlewares.md | 44 +++--- internal/api/handler.go | 5 +- internal/common/env.go | 13 +- internal/docker/label.go | 43 ------ internal/docker/label_parser.go | 87 ------------ internal/docker/label_parser_test.go | 106 -------------- internal/docker/label_test.go | 12 +- internal/docker/labels.go | 2 + .../net/http/middleware/cidr_whitelist.go | 9 +- .../net/http/middleware/cloudflare_real_ip.go | 4 +- internal/net/http/middleware/forward_auth.go | 15 +- internal/net/http/middleware/middleware.go | 8 +- internal/net/http/middleware/middlewares.go | 6 - .../net/http/middleware/modify_request.go | 15 +- .../net/http/middleware/modify_response.go | 15 +- internal/net/http/middleware/real_ip.go | 9 +- internal/utils/serialization.go | 130 ++++++++++++++---- internal/utils/string.go | 6 + 20 files changed, 201 insertions(+), 371 deletions(-) delete mode 100644 internal/docker/label_parser.go delete mode 100644 internal/docker/label_parser_test.go diff --git a/cmd/main.go b/cmd/main.go index 320212d..8a50ef1 100755 --- a/cmd/main.go +++ b/cmd/main.go @@ -116,10 +116,6 @@ func main() { printJSON(trace) } - if common.IsDebug { - printJSON(docker.GetRegisteredNamespaces()) - } - cfg.StartProxyProviders() cfg.WatchChanges() diff --git a/docs/docker.md b/docs/docker.md index fe14cba..e113992 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -6,6 +6,7 @@ - [Docker compose guide](#docker-compose-guide) - [Table of content](#table-of-content) + - [Suggestions](#suggestions) - [Additional setup](#additional-setup) - [Labels](#labels) - [Syntax](#syntax) @@ -16,6 +17,30 @@ - [Docker compose examples](#docker-compose-examples) - [Services URLs for above examples](#services-urls-for-above-examples) +## Suggestions + +In order for labels to work correctly in `compose.yml`: + +1. `key: value` mapping is suggested for label, instead of `- key=value` +2. you need to add `|` in the end for multiline strings. + +Example + +```yaml +services: + app: + ... + container_name: app + labels: + proxy.app.middlewares.modify_request.set_headers: | + X-Custom-Header1: value1, value2 + X-Custom-Header2: value3 + proxy.app.middlewares.modify_request.hide_headers: | + X-Custom-Header4 + X-Custom-Header5 + X-Custom-Header6 +``` + ## Additional setup 1. Enable HTTPs _(optional)_ @@ -89,7 +114,7 @@ | `port` | proxy port **(http/s)** | first port returned from docker | number in range of `1 - 65535` | | `port` | proxy port **(tcp/udp)** | `0:first_port` | `x:y`
| | `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean | -| `path_patterns` | proxy path patterns **(http/s only)**
only requests that matched a pattern will be proxied | `/` **(proxy all requests)** | yaml style list[1](#list-example) of ([path patterns](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) | +| `path_patterns` | proxy path patterns **(http/s only)**
only requests that matched a pattern will be proxied | `/` **(proxy all requests)** | list[1](#list-example) of ([path patterns](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) | [🔼Back to top](#table-of-content) @@ -132,11 +157,11 @@ services: ... labels: proxy.nginx.path_patterns: | # remember to add the '|' - - GET / - - POST /auth + GET / + POST /auth proxy.nginx.middlewares.modify_request.hide_headers: | # remember to add the '|' - - X-Custom-Header1 - - X-Custom-Header2 + X-Custom-Header1 + X-Custom-Header2 ``` Include file @@ -145,8 +170,8 @@ Include file service_a: host: service_a.internal path_patterns: - - GET / - - POST /auth + GET / + POST /auth middlewares: modify_request: hide_headers: diff --git a/docs/middlewares.md b/docs/middlewares.md index 64615a2..0241708 100644 --- a/docs/middlewares.md +++ b/docs/middlewares.md @@ -119,9 +119,9 @@ Check https://nginx.org/en/docs/http/ngx_http_realip_module.html for explainatio # docker labels proxy.app1.middlewares.real_ip.header: X-Real-IP proxy.app1.middlewares.real_ip.from: | - - 127.0.0.1 - - 192.168.0.0/16 - - 10.0.0.0/8 + 127.0.0.1 + 192.168.0.0/16 + 10.0.0.0/8 proxy.app1.middlewares.real_ip.recursive: true # include file @@ -177,8 +177,8 @@ app1: ```yaml # docker labels proxy.app1.middlewares.cidr_whitelist.allow: | - - 10.0.0.0/8 - - 192.168.0.0/16 + 10.0.0.0/8 + 192.168.0.0/16 # optional (default: 403) proxy.app1.middlewares.cidr_whitelist.status_code: 403 # optional (default: "IP not allowed") @@ -270,8 +270,8 @@ location / { ```yaml # docker labels proxy.app1.middlewares.modify_request.hide_headers: | - - X-Custom-Header1 - - X-Custom-Header2 + X-Custom-Header1 + X-Custom-Header2 # include file app1: @@ -339,11 +339,11 @@ Fields: proxy.app1.middlewares.forward_auth.address: https://auth.example.com proxy.app1.middlewares.forward_auth.trust_forward_header: true proxy.app1.middlewares.forward_auth.auth_response_headers: | - - X-Auth-Token - - X-Auth-User + X-Auth-Token + X-Auth-User proxy.app1.middlewares.forward_auth.add_auth_cookies_to_response: | - - uid - - session_id + uid + session_id # include file app1: @@ -421,17 +421,17 @@ services: proxy.#1.middlewares.forward_auth.address: https://your_authentik_forward_address proxy.#1.middlewares.forward_auth.trustForwardHeader: true proxy.#1.middlewares.forward_auth.authResponseHeaders: | - - X-authentik-username - - X-authentik-groups - - X-authentik-email - - X-authentik-name - - X-authentik-uid - - X-authentik-jwt - - X-authentik-meta-jwks - - X-authentik-meta-outpost - - X-authentik-meta-provider - - X-authentik-meta-app - - X-authentik-meta-version + X-authentik-username + X-authentik-groups + X-authentik-email + X-authentik-name + X-authentik-uid + X-authentik-jwt + X-authentik-meta-jwks + X-authentik-meta-outpost + X-authentik-meta-provider + X-authentik-meta-app + X-authentik-meta-version restart: unless-stopped ``` diff --git a/internal/api/handler.go b/internal/api/handler.go index af76837..271c40d 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -40,10 +40,13 @@ func NewHandler(cfg *config.Config) http.Handler { // allow only requests to API server with host matching common.APIHTTPAddr func checkHost(f http.HandlerFunc) http.HandlerFunc { + if common.IsDebug { + return f + } return func(w http.ResponseWriter, r *http.Request) { if r.Host != common.APIHTTPAddr { Logger.Warnf("invalid request to API server with host: %s, expect %s", r.Host, common.APIHTTPAddr) - w.WriteHeader(http.StatusNotFound) + w.WriteHeader(http.StatusForbidden) w.Write([]byte("invalid request")) return } diff --git a/internal/common/env.go b/internal/common/env.go index ddfe935..5c5c3ba 100644 --- a/internal/common/env.go +++ b/internal/common/env.go @@ -2,8 +2,10 @@ package common import ( "fmt" + "log" "net" "os" + "strconv" "strings" "github.com/sirupsen/logrus" @@ -35,14 +37,11 @@ func GetEnvBool(key string, defaultValue bool) bool { if !ok || value == "" { return defaultValue } - switch strings.ToLower(value) { - case "true", "yes", "1": - return true - case "false", "no", "0": - return false - default: - return defaultValue + b, err := strconv.ParseBool(value) + if err != nil { + log.Fatalf("Invalid boolean value: %s", value) } + return b } func GetEnv(key, defaultValue string) string { diff --git a/internal/docker/label.go b/internal/docker/label.go index 8326622..63e072f 100644 --- a/internal/docker/label.go +++ b/internal/docker/label.go @@ -6,7 +6,6 @@ import ( E "github.com/yusing/go-proxy/internal/error" U "github.com/yusing/go-proxy/internal/utils" - F "github.com/yusing/go-proxy/internal/utils/functional" ) /* @@ -23,8 +22,6 @@ type ( Value any } NestedLabelMap map[string]U.SerializedObject - ValueParser func(string) (any, E.NestedError) - ValueParserMap map[string]ValueParser ) func (l *Label) String() string { @@ -107,45 +104,5 @@ func ParseLabel(label string, value string) (*Label, E.NestedError) { l.Value = nestedLabel } - // find if namespace has value parser - pm, ok := valueParserMap.Load(U.ToLowerNoSnake(l.Namespace)) - if !ok { - return l, nil - } - // find if attribute has value parser - p, ok := pm[U.ToLowerNoSnake(l.Attribute)] - if !ok { - return l, nil - } - // try to parse value - v, err := p(value) - if err.HasError() { - return nil, err.Subject(label) - } - l.Value = v return l, nil } - -func RegisterNamespace(namespace string, pm ValueParserMap) { - pmCleaned := make(ValueParserMap, len(pm)) - for k, v := range pm { - pmCleaned[U.ToLowerNoSnake(k)] = v - } - valueParserMap.Store(U.ToLowerNoSnake(namespace), pmCleaned) -} - -func GetRegisteredNamespaces() map[string][]string { - r := make(map[string][]string) - - valueParserMap.RangeAll(func(ns string, vpm ValueParserMap) { - r[ns] = make([]string, 0, len(vpm)) - for attr := range vpm { - r[ns] = append(r[ns], attr) - } - }) - - return r -} - -// namespace:target.attribute -> func(string) (any, error) -var valueParserMap = F.NewMapOf[string, ValueParserMap]() diff --git a/internal/docker/label_parser.go b/internal/docker/label_parser.go deleted file mode 100644 index b166216..0000000 --- a/internal/docker/label_parser.go +++ /dev/null @@ -1,87 +0,0 @@ -package docker - -import ( - "strconv" - "strings" - - E "github.com/yusing/go-proxy/internal/error" - "gopkg.in/yaml.v3" -) - -const ( - NSProxy = "proxy" - ProxyAttributePathPatterns = "path_patterns" - ProxyAttributeNoTLSVerify = "no_tls_verify" - ProxyAttributeMiddlewares = "middlewares" -) - -var _ = func() int { - RegisterNamespace(NSProxy, ValueParserMap{ - ProxyAttributePathPatterns: YamlStringListParser, - ProxyAttributeNoTLSVerify: BoolParser, - }) - return 0 -}() - -func YamlStringListParser(value string) (any, E.NestedError) { - /* - - foo - - bar - - baz - */ - value = strings.TrimSpace(value) - if value == "" { - return []string{}, nil - } - var data []string - err := E.From(yaml.Unmarshal([]byte(value), &data)) - return data, err -} - -func YamlLikeMappingParser(allowDuplicate bool) func(string) (any, E.NestedError) { - return func(value string) (any, E.NestedError) { - /* - foo: bar - boo: baz - */ - value = strings.TrimSpace(value) - lines := strings.Split(value, "\n") - h := make(map[string]string) - for _, line := range lines { - parts := strings.SplitN(line, ":", 2) - if len(parts) != 2 { - return nil, E.Invalid("syntax", line).With("too many colons") - } - key := strings.TrimSpace(parts[0]) - val := strings.TrimSpace(parts[1]) - if existing, ok := h[key]; ok { - if !allowDuplicate { - return nil, E.Duplicated("key", key) - } - h[key] = existing + ", " + val - } else { - h[key] = val - } - } - return h, nil - } -} - -func BoolParser(value string) (any, E.NestedError) { - switch strings.ToLower(value) { - case "true", "yes", "1": - return true, nil - case "false", "no", "0": - return false, nil - default: - return nil, E.Invalid("boolean value", value) - } -} - -func IntParser(value string) (any, E.NestedError) { - i, err := strconv.Atoi(value) - if err != nil { - return 0, E.Invalid("integer value", value) - } - return i, nil -} diff --git a/internal/docker/label_parser_test.go b/internal/docker/label_parser_test.go deleted file mode 100644 index 1fd9ce8..0000000 --- a/internal/docker/label_parser_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package docker - -import ( - "fmt" - "testing" - - E "github.com/yusing/go-proxy/internal/error" - . "github.com/yusing/go-proxy/internal/utils/testing" -) - -func makeLabel(namespace string, alias string, field string) string { - return fmt.Sprintf("%s.%s.%s", namespace, alias, field) -} - -func TestParseLabel(t *testing.T) { - alias := "foo" - field := "ip" - v := "bar" - pl, err := ParseLabel(makeLabel(NSHomePage, alias, field), v) - ExpectNoError(t, err.Error()) - ExpectEqual(t, pl.Namespace, NSHomePage) - ExpectEqual(t, pl.Target, alias) - ExpectEqual(t, pl.Attribute, field) - ExpectEqual(t, pl.Value.(string), v) -} - -func TestStringProxyLabel(t *testing.T) { - v := "bar" - pl, err := ParseLabel(makeLabel(NSProxy, "foo", "ip"), v) - ExpectNoError(t, err.Error()) - ExpectEqual(t, pl.Value.(string), v) -} - -func TestBoolProxyLabelValid(t *testing.T) { - tests := map[string]bool{ - "true": true, - "TRUE": true, - "yes": true, - "1": true, - "false": false, - "FALSE": false, - "no": false, - "0": false, - } - - for k, v := range tests { - pl, err := ParseLabel(makeLabel(NSProxy, "foo", ProxyAttributeNoTLSVerify), k) - ExpectNoError(t, err.Error()) - ExpectEqual(t, pl.Value.(bool), v) - } -} - -func TestBoolProxyLabelInvalid(t *testing.T) { - _, err := ParseLabel(makeLabel(NSProxy, "foo", ProxyAttributeNoTLSVerify), "invalid") - if !err.Is(E.ErrInvalid) { - t.Errorf("Expected err InvalidProxyLabel, got %s", err.Error()) - } -} - -// func TestSetHeaderProxyLabelValid(t *testing.T) { -// v := ` -// X-Custom-Header1: foo, bar -// X-Custom-Header1: baz -// X-Custom-Header2: boo` -// v = strings.TrimPrefix(v, "\n") -// h := map[string]string{ -// "X-Custom-Header1": "foo, bar, baz", -// "X-Custom-Header2": "boo", -// } - -// pl, err := ParseLabel(makeLabel(NSProxy, "foo", ProxyAttributeSetHeaders), v) -// ExpectNoError(t, err.Error()) -// hGot := ExpectType[map[string]string](t, pl.Value) -// ExpectFalse(t, hGot == nil) -// ExpectDeepEqual(t, h, hGot) -// } - -// func TestSetHeaderProxyLabelInvalid(t *testing.T) { -// tests := []string{ -// "X-Custom-Header1 = bar", -// "X-Custom-Header1", -// "- X-Custom-Header1", -// } - -// for _, v := range tests { -// _, err := ParseLabel(makeLabel(NSProxy, "foo", ProxyAttributeSetHeaders), v) -// if !err.Is(E.ErrInvalid) { -// t.Errorf("Expected invalid err for %q, got %s", v, err.Error()) -// } -// } -// } - -// func TestHideHeadersProxyLabel(t *testing.T) { -// v := ` -// - X-Custom-Header1 -// - X-Custom-Header2 -// - X-Custom-Header3 -// ` -// v = strings.TrimPrefix(v, "\n") -// pl, err := ParseLabel(makeLabel(NSProxy, "foo", ProxyAttributeHideHeaders), v) -// ExpectNoError(t, err.Error()) -// sGot := ExpectType[[]string](t, pl.Value) -// sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"} -// ExpectFalse(t, sGot == nil) -// ExpectDeepEqual(t, sGot, sWant) -// } diff --git a/internal/docker/label_test.go b/internal/docker/label_test.go index f8fe7aa..c2588be 100644 --- a/internal/docker/label_test.go +++ b/internal/docker/label_test.go @@ -8,11 +8,15 @@ import ( . "github.com/yusing/go-proxy/internal/utils/testing" ) +func makeLabel(ns, name, attr string) string { + return fmt.Sprintf("%s.%s.%s", ns, name, attr) +} + func TestNestedLabel(t *testing.T) { mName := "middleware1" mAttr := "prop1" v := "value1" - pl, err := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s.%s", ProxyAttributeMiddlewares, mName, mAttr)), v) + pl, err := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v) ExpectNoError(t, err.Error()) sGot := ExpectType[*Label](t, pl.Value) ExpectFalse(t, sGot == nil) @@ -27,7 +31,7 @@ func TestApplyNestedLabel(t *testing.T) { mName := "middleware1" mAttr := "prop1" v := "value1" - pl, err := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s.%s", ProxyAttributeMiddlewares, mName, mAttr)), v) + pl, err := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v) ExpectNoError(t, err.Error()) err = ApplyLabel(entry, pl) ExpectNoError(t, err.Error()) @@ -51,7 +55,7 @@ func TestApplyNestedLabelExisting(t *testing.T) { entry.Middlewares[mName] = make(U.SerializedObject) entry.Middlewares[mName][checkAttr] = checkV - pl, err := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s.%s", ProxyAttributeMiddlewares, mName, mAttr)), v) + pl, err := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v) ExpectNoError(t, err.Error()) err = ApplyLabel(entry, pl) ExpectNoError(t, err.Error()) @@ -76,7 +80,7 @@ func TestApplyNestedLabelNoAttr(t *testing.T) { entry.Middlewares = make(NestedLabelMap) entry.Middlewares[mName] = make(U.SerializedObject) - pl, err := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s", ProxyAttributeMiddlewares, mName)), v) + pl, err := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s", "middlewares", mName)), v) ExpectNoError(t, err.Error()) err = ApplyLabel(entry, pl) ExpectNoError(t, err.Error()) diff --git a/internal/docker/labels.go b/internal/docker/labels.go index 444db36..8c78e79 100644 --- a/internal/docker/labels.go +++ b/internal/docker/labels.go @@ -3,6 +3,8 @@ package docker const ( WildcardAlias = "*" + NSProxy = "proxy" + LabelAliases = NSProxy + ".aliases" LabelExclude = NSProxy + ".exclude" LabelIdleTimeout = NSProxy + ".idle_timeout" diff --git a/internal/net/http/middleware/cidr_whitelist.go b/internal/net/http/middleware/cidr_whitelist.go index 2d6e324..3a5cfe5 100644 --- a/internal/net/http/middleware/cidr_whitelist.go +++ b/internal/net/http/middleware/cidr_whitelist.go @@ -4,7 +4,6 @@ import ( "net" "net/http" - D "github.com/yusing/go-proxy/internal/docker" E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/types" F "github.com/yusing/go-proxy/internal/utils/functional" @@ -24,13 +23,7 @@ type cidrWhitelistOpts struct { } var CIDRWhiteList = &cidrWhitelist{ - m: &Middleware{ - labelParserMap: D.ValueParserMap{ - "allow": D.YamlStringListParser, - "statusCode": D.IntParser, - }, - withOptions: NewCIDRWhitelist, - }, + m: &Middleware{withOptions: NewCIDRWhitelist}, } var cidrWhitelistDefaults = func() *cidrWhitelistOpts { diff --git a/internal/net/http/middleware/cloudflare_real_ip.go b/internal/net/http/middleware/cloudflare_real_ip.go index 96101f9..cd7f64c 100644 --- a/internal/net/http/middleware/cloudflare_real_ip.go +++ b/internal/net/http/middleware/cloudflare_real_ip.go @@ -30,9 +30,7 @@ var ( ) var CloudflareRealIP = &realIP{ - m: &Middleware{ - withOptions: NewCloudflareRealIP, - }, + m: &Middleware{withOptions: NewCloudflareRealIP}, } func NewCloudflareRealIP(_ OptionsRaw) (*Middleware, E.NestedError) { diff --git a/internal/net/http/middleware/forward_auth.go b/internal/net/http/middleware/forward_auth.go index ca9e906..dbb89f9 100644 --- a/internal/net/http/middleware/forward_auth.go +++ b/internal/net/http/middleware/forward_auth.go @@ -13,7 +13,6 @@ import ( "strings" "time" - D "github.com/yusing/go-proxy/internal/docker" E "github.com/yusing/go-proxy/internal/error" gpHTTP "github.com/yusing/go-proxy/internal/net/http" ) @@ -33,17 +32,9 @@ type ( } ) -var ForwardAuth = func() *forwardAuth { - fa := new(forwardAuth) - fa.m = new(Middleware) - fa.m.labelParserMap = D.ValueParserMap{ - "trust_forward_header": D.BoolParser, - "auth_response_headers": D.YamlStringListParser, - "add_auth_cookies_to_response": D.YamlStringListParser, - } - fa.m.withOptions = NewForwardAuthfunc - return fa -}() +var ForwardAuth = &forwardAuth{ + m: &Middleware{withOptions: NewForwardAuthfunc}, +} func NewForwardAuthfunc(optsRaw OptionsRaw) (*Middleware, E.NestedError) { faWithOpts := new(forwardAuth) diff --git a/internal/net/http/middleware/middleware.go b/internal/net/http/middleware/middleware.go index 3908c96..1058f1c 100644 --- a/internal/net/http/middleware/middleware.go +++ b/internal/net/http/middleware/middleware.go @@ -5,7 +5,6 @@ import ( "errors" "net/http" - D "github.com/yusing/go-proxy/internal/docker" E "github.com/yusing/go-proxy/internal/error" gpHTTP "github.com/yusing/go-proxy/internal/net/http" U "github.com/yusing/go-proxy/internal/utils" @@ -36,9 +35,8 @@ type ( before BeforeFunc // runs before ReverseProxy.ServeHTTP modifyResponse ModifyResponseFunc // runs after ReverseProxy.ModifyResponse - withOptions CloneWithOptFunc - labelParserMap D.ValueParserMap - impl any + withOptions CloneWithOptFunc + impl any parent *Middleware children []*Middleware @@ -92,7 +90,7 @@ func (m *Middleware) WithOptionsClone(optsRaw OptionsRaw) (*Middleware, E.Nested m.name, m.before, m.modifyResponse, - nil, nil, + nil, m.impl, m.parent, m.children, diff --git a/internal/net/http/middleware/middlewares.go b/internal/net/http/middleware/middlewares.go index aebec82..60a1a84 100644 --- a/internal/net/http/middleware/middlewares.go +++ b/internal/net/http/middleware/middlewares.go @@ -8,7 +8,6 @@ import ( "github.com/sirupsen/logrus" "github.com/yusing/go-proxy/internal/common" - D "github.com/yusing/go-proxy/internal/docker" E "github.com/yusing/go-proxy/internal/error" U "github.com/yusing/go-proxy/internal/utils" ) @@ -42,11 +41,6 @@ func init() { names := make(map[*Middleware][]string) for name, m := range middlewares { names[m] = append(names[m], http.CanonicalHeaderKey(name)) - // register middleware name to docker label parsr - // in order to parse middleware_name.option=value into correct type - if m.labelParserMap != nil { - D.RegisterNamespace(name, m.labelParserMap) - } } for m, names := range names { if len(names) > 1 { diff --git a/internal/net/http/middleware/modify_request.go b/internal/net/http/middleware/modify_request.go index 69febf9..261d4f0 100644 --- a/internal/net/http/middleware/modify_request.go +++ b/internal/net/http/middleware/modify_request.go @@ -2,7 +2,6 @@ package middleware import ( "github.com/yusing/go-proxy/internal/common" - D "github.com/yusing/go-proxy/internal/docker" E "github.com/yusing/go-proxy/internal/error" ) @@ -19,17 +18,9 @@ type ( } ) -var ModifyRequest = func() *modifyRequest { - mr := new(modifyRequest) - mr.m = new(Middleware) - mr.m.labelParserMap = D.ValueParserMap{ - "set_headers": D.YamlLikeMappingParser(true), - "add_headers": D.YamlLikeMappingParser(true), - "hide_headers": D.YamlStringListParser, - } - mr.m.withOptions = NewModifyRequest - return mr -}() +var ModifyRequest = &modifyRequest{ + m: &Middleware{withOptions: NewModifyRequest}, +} func NewModifyRequest(optsRaw OptionsRaw) (*Middleware, E.NestedError) { mr := new(modifyRequest) diff --git a/internal/net/http/middleware/modify_response.go b/internal/net/http/middleware/modify_response.go index ecf7b0a..dd2ad24 100644 --- a/internal/net/http/middleware/modify_response.go +++ b/internal/net/http/middleware/modify_response.go @@ -4,7 +4,6 @@ import ( "net/http" "github.com/yusing/go-proxy/internal/common" - D "github.com/yusing/go-proxy/internal/docker" E "github.com/yusing/go-proxy/internal/error" ) @@ -21,17 +20,9 @@ type ( } ) -var ModifyResponse = func() (mr *modifyResponse) { - mr = new(modifyResponse) - mr.m = new(Middleware) - mr.m.labelParserMap = D.ValueParserMap{ - "set_headers": D.YamlLikeMappingParser(true), - "add_headers": D.YamlLikeMappingParser(true), - "hide_headers": D.YamlStringListParser, - } - mr.m.withOptions = NewModifyResponse - return -}() +var ModifyResponse = &modifyResponse{ + m: &Middleware{withOptions: NewModifyResponse}, +} func NewModifyResponse(optsRaw OptionsRaw) (*Middleware, E.NestedError) { mr := new(modifyResponse) diff --git a/internal/net/http/middleware/real_ip.go b/internal/net/http/middleware/real_ip.go index 75bfb82..ff5691f 100644 --- a/internal/net/http/middleware/real_ip.go +++ b/internal/net/http/middleware/real_ip.go @@ -3,7 +3,6 @@ package middleware import ( "net" - D "github.com/yusing/go-proxy/internal/docker" E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/types" ) @@ -32,13 +31,7 @@ type realIPOpts struct { } var RealIP = &realIP{ - m: &Middleware{ - labelParserMap: D.ValueParserMap{ - "from": D.YamlStringListParser, - "recursive": D.BoolParser, - }, - withOptions: NewRealIP, - }, + m: &Middleware{withOptions: NewRealIP}, } var realIPOptsDefault = func() *realIPOpts { diff --git a/internal/utils/serialization.go b/internal/utils/serialization.go index fb0f240..b9a7f32 100644 --- a/internal/utils/serialization.go +++ b/internal/utils/serialization.go @@ -5,7 +5,9 @@ import ( "encoding/json" "fmt" "reflect" + "strconv" "strings" + "unicode" "github.com/santhosh-tekuri/jsonschema" E "github.com/yusing/go-proxy/internal/error" @@ -210,17 +212,16 @@ func Convert(src reflect.Value, dst reflect.Value) E.NestedError { switch { case srcT.AssignableTo(dstT): dst.Set(src) + return nil case srcT.ConvertibleTo(dstT): dst.Set(src.Convert(dstT)) + return nil case srcT.Kind() == reflect.Map: obj, ok := src.Interface().(SerializedObject) if !ok { return E.TypeMismatch[SerializedObject](src.Interface()) } - err := Deserialize(obj, dst.Addr().Interface()) - if err != nil { - return err - } + return Deserialize(obj, dst.Addr().Interface()) case srcT.Kind() == reflect.Slice: if dstT.Kind() != reflect.Slice { return E.TypeError("slice", srcT, dstT) @@ -237,33 +238,114 @@ func Convert(src reflect.Value, dst reflect.Value) E.NestedError { i++ } dst.Set(newSlice) - default: - var converter Converter - var ok bool - // check if (*T).Convertor is implemented - if converter, ok = dst.Addr().Interface().(Converter); !ok { - // check if (T).Convertor is implemented - converter, ok = dst.Interface().(Converter) - if !ok { - return E.TypeError("conversion", srcT, dstT) - } - } - - converted, err := converter.ConvertFrom(src.Interface()) - if err != nil { + return nil + case src.Kind() == reflect.String: + if convertible, err := ConvertString(src.String(), dst); convertible { return err } - c := reflect.ValueOf(converted) - if c.Kind() == reflect.Ptr { - c = c.Elem() - } - dst.Set(c) - return nil } + var converter Converter + var ok bool + // check if (*T).Convertor is implemented + if converter, ok = dst.Addr().Interface().(Converter); !ok { + // check if (T).Convertor is implemented + converter, ok = dst.Interface().(Converter) + if !ok { + return E.TypeError("conversion", srcT, dstT) + } + } + + converted, err := converter.ConvertFrom(src.Interface()) + if err != nil { + return err + } + c := reflect.ValueOf(converted) + if c.Kind() == reflect.Ptr { + c = c.Elem() + } + dst.Set(c) return nil } +func ConvertString(src string, dst reflect.Value) (convertible bool, convErr E.NestedError) { + convertible = true + // primitive types / simple types + switch dst.Kind() { + case reflect.Bool: + b, err := strconv.ParseBool(src) + if err != nil { + convErr = E.Invalid("boolean", src) + return + } + dst.Set(reflect.ValueOf(b)) + return + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(src, 10, 64) + if err != nil { + convErr = E.Invalid("int", src) + return + } + dst.Set(reflect.ValueOf(i)) + return + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + i, err := strconv.ParseUint(src, 10, 64) + if err != nil { + convErr = E.Invalid("uint", src) + return + } + dst.Set(reflect.ValueOf(i)) + return + } + // yaml like + lines := strings.Split(strings.TrimSpace(src), "\n") + for i := range lines { + lines[i] = strings.TrimSpace(lines[i]) + } + var tmp any + switch dst.Kind() { + case reflect.Slice: + // one liner is comma seperated list + if len(lines) == 0 { + dst.Set(reflect.ValueOf(CommaSeperatedList(src))) + return + } + sl := make([]string, 0, len(lines)) + for _, line := range lines { + line = strings.TrimLeftFunc(line, func(r rune) bool { + return r == '-' || unicode.IsSpace(r) + }) + if line == "" { + continue + } + sl = append(sl, line) + } + tmp = sl + case reflect.Map: + m := make(map[string]string, len(lines)) + for i, line := range lines { + parts := strings.Split(line, ":") + if len(parts) < 2 { + convErr = E.Invalid("map", "missing colon").Subjectf("line#%d", i+1).With(line) + return + } + if len(parts) > 2 { + convErr = E.Invalid("map", "too many colons").Subjectf("line#%d", i+1).With(line) + return + } + k := strings.TrimSpace(parts[0]) + v := strings.TrimSpace(parts[1]) + m[k] = v + } + tmp = m + } + if tmp == nil { + convertible = false + return + } + return true, Convert(reflect.ValueOf(tmp), dst) +} + func DeserializeJson(j map[string]string, target any) E.NestedError { data, err := E.Check(json.Marshal(j)) if err != nil { diff --git a/internal/utils/string.go b/internal/utils/string.go index 5938d85..ccca361 100644 --- a/internal/utils/string.go +++ b/internal/utils/string.go @@ -4,6 +4,8 @@ import ( "net/url" "strconv" "strings" + + E "github.com/yusing/go-proxy/internal/error" ) func CommaSeperatedList(s string) []string { @@ -14,6 +16,10 @@ func CommaSeperatedList(s string) []string { return res } +func IntParser(value string) (int, E.NestedError) { + return E.Check(strconv.Atoi(value)) +} + func ExtractPort(fullURL string) (int, error) { url, err := url.Parse(fullURL) if err != nil {