replacing label parser map with improved deserialization implementation, API host check now disabled when in debug mode

This commit is contained in:
yusing 2024-10-03 01:50:49 +08:00
parent ef52ccb929
commit 8329a8ea9c
20 changed files with 201 additions and 371 deletions

View file

@ -116,10 +116,6 @@ func main() {
printJSON(trace) printJSON(trace)
} }
if common.IsDebug {
printJSON(docker.GetRegisteredNamespaces())
}
cfg.StartProxyProviders() cfg.StartProxyProviders()
cfg.WatchChanges() cfg.WatchChanges()

View file

@ -6,6 +6,7 @@
- [Docker compose guide](#docker-compose-guide) - [Docker compose guide](#docker-compose-guide)
- [Table of content](#table-of-content) - [Table of content](#table-of-content)
- [Suggestions](#suggestions)
- [Additional setup](#additional-setup) - [Additional setup](#additional-setup)
- [Labels](#labels) - [Labels](#labels)
- [Syntax](#syntax) - [Syntax](#syntax)
@ -16,6 +17,30 @@
- [Docker compose examples](#docker-compose-examples) - [Docker compose examples](#docker-compose-examples)
- [Services URLs for above examples](#services-urls-for-above-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 ## Additional setup
1. Enable HTTPs _(optional)_ 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 **(http/s)** | first port returned from docker | number in range of `1 - 65535` |
| `port` | proxy port **(tcp/udp)** | `0:first_port` | `x:y` <br><ul><li>**x**: port for `go-proxy` to listen on.<br>**x** can be 0, which means listen on a random port</li><li>**y**: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> | | `port` | proxy port **(tcp/udp)** | `0:first_port` | `x:y` <br><ul><li>**x**: port for `go-proxy` to listen on.<br>**x** can be 0, which means listen on a random port</li><li>**y**: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> |
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean | | `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
| `path_patterns` | proxy path patterns **(http/s only)**<br> only requests that matched a pattern will be proxied | `/` **(proxy all requests)** | yaml style list[<sup>1</sup>](#list-example) of ([path patterns](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) | | `path_patterns` | proxy path patterns **(http/s only)**<br> only requests that matched a pattern will be proxied | `/` **(proxy all requests)** | list[<sup>1</sup>](#list-example) of ([path patterns](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) |
[🔼Back to top](#table-of-content) [🔼Back to top](#table-of-content)
@ -132,11 +157,11 @@ services:
... ...
labels: labels:
proxy.nginx.path_patterns: | # remember to add the '|' proxy.nginx.path_patterns: | # remember to add the '|'
- GET / GET /
- POST /auth POST /auth
proxy.nginx.middlewares.modify_request.hide_headers: | # remember to add the '|' proxy.nginx.middlewares.modify_request.hide_headers: | # remember to add the '|'
- X-Custom-Header1 X-Custom-Header1
- X-Custom-Header2 X-Custom-Header2
``` ```
Include file Include file
@ -145,8 +170,8 @@ Include file
service_a: service_a:
host: service_a.internal host: service_a.internal
path_patterns: path_patterns:
- GET / GET /
- POST /auth POST /auth
middlewares: middlewares:
modify_request: modify_request:
hide_headers: hide_headers:

View file

@ -119,9 +119,9 @@ Check https://nginx.org/en/docs/http/ngx_http_realip_module.html for explainatio
# docker labels # docker labels
proxy.app1.middlewares.real_ip.header: X-Real-IP proxy.app1.middlewares.real_ip.header: X-Real-IP
proxy.app1.middlewares.real_ip.from: | proxy.app1.middlewares.real_ip.from: |
- 127.0.0.1 127.0.0.1
- 192.168.0.0/16 192.168.0.0/16
- 10.0.0.0/8 10.0.0.0/8
proxy.app1.middlewares.real_ip.recursive: true proxy.app1.middlewares.real_ip.recursive: true
# include file # include file
@ -177,8 +177,8 @@ app1:
```yaml ```yaml
# docker labels # docker labels
proxy.app1.middlewares.cidr_whitelist.allow: | proxy.app1.middlewares.cidr_whitelist.allow: |
- 10.0.0.0/8 10.0.0.0/8
- 192.168.0.0/16 192.168.0.0/16
# optional (default: 403) # optional (default: 403)
proxy.app1.middlewares.cidr_whitelist.status_code: 403 proxy.app1.middlewares.cidr_whitelist.status_code: 403
# optional (default: "IP not allowed") # optional (default: "IP not allowed")
@ -270,8 +270,8 @@ location / {
```yaml ```yaml
# docker labels # docker labels
proxy.app1.middlewares.modify_request.hide_headers: | proxy.app1.middlewares.modify_request.hide_headers: |
- X-Custom-Header1 X-Custom-Header1
- X-Custom-Header2 X-Custom-Header2
# include file # include file
app1: app1:
@ -339,11 +339,11 @@ Fields:
proxy.app1.middlewares.forward_auth.address: https://auth.example.com proxy.app1.middlewares.forward_auth.address: https://auth.example.com
proxy.app1.middlewares.forward_auth.trust_forward_header: true proxy.app1.middlewares.forward_auth.trust_forward_header: true
proxy.app1.middlewares.forward_auth.auth_response_headers: | proxy.app1.middlewares.forward_auth.auth_response_headers: |
- X-Auth-Token X-Auth-Token
- X-Auth-User X-Auth-User
proxy.app1.middlewares.forward_auth.add_auth_cookies_to_response: | proxy.app1.middlewares.forward_auth.add_auth_cookies_to_response: |
- uid uid
- session_id session_id
# include file # include file
app1: app1:
@ -421,17 +421,17 @@ services:
proxy.#1.middlewares.forward_auth.address: https://your_authentik_forward_address proxy.#1.middlewares.forward_auth.address: https://your_authentik_forward_address
proxy.#1.middlewares.forward_auth.trustForwardHeader: true proxy.#1.middlewares.forward_auth.trustForwardHeader: true
proxy.#1.middlewares.forward_auth.authResponseHeaders: | proxy.#1.middlewares.forward_auth.authResponseHeaders: |
- X-authentik-username X-authentik-username
- X-authentik-groups X-authentik-groups
- X-authentik-email X-authentik-email
- X-authentik-name X-authentik-name
- X-authentik-uid X-authentik-uid
- X-authentik-jwt X-authentik-jwt
- X-authentik-meta-jwks X-authentik-meta-jwks
- X-authentik-meta-outpost X-authentik-meta-outpost
- X-authentik-meta-provider X-authentik-meta-provider
- X-authentik-meta-app X-authentik-meta-app
- X-authentik-meta-version X-authentik-meta-version
restart: unless-stopped restart: unless-stopped
``` ```

View file

@ -40,10 +40,13 @@ func NewHandler(cfg *config.Config) http.Handler {
// allow only requests to API server with host matching common.APIHTTPAddr // allow only requests to API server with host matching common.APIHTTPAddr
func checkHost(f http.HandlerFunc) http.HandlerFunc { func checkHost(f http.HandlerFunc) http.HandlerFunc {
if common.IsDebug {
return f
}
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if r.Host != common.APIHTTPAddr { if r.Host != common.APIHTTPAddr {
Logger.Warnf("invalid request to API server with host: %s, expect %s", 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")) w.Write([]byte("invalid request"))
return return
} }

View file

@ -2,8 +2,10 @@ package common
import ( import (
"fmt" "fmt"
"log"
"net" "net"
"os" "os"
"strconv"
"strings" "strings"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -35,14 +37,11 @@ func GetEnvBool(key string, defaultValue bool) bool {
if !ok || value == "" { if !ok || value == "" {
return defaultValue return defaultValue
} }
switch strings.ToLower(value) { b, err := strconv.ParseBool(value)
case "true", "yes", "1": if err != nil {
return true log.Fatalf("Invalid boolean value: %s", value)
case "false", "no", "0":
return false
default:
return defaultValue
} }
return b
} }
func GetEnv(key, defaultValue string) string { func GetEnv(key, defaultValue string) string {

View file

@ -6,7 +6,6 @@ import (
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
U "github.com/yusing/go-proxy/internal/utils" U "github.com/yusing/go-proxy/internal/utils"
F "github.com/yusing/go-proxy/internal/utils/functional"
) )
/* /*
@ -23,8 +22,6 @@ type (
Value any Value any
} }
NestedLabelMap map[string]U.SerializedObject NestedLabelMap map[string]U.SerializedObject
ValueParser func(string) (any, E.NestedError)
ValueParserMap map[string]ValueParser
) )
func (l *Label) String() string { func (l *Label) String() string {
@ -107,45 +104,5 @@ func ParseLabel(label string, value string) (*Label, E.NestedError) {
l.Value = nestedLabel l.Value = nestedLabel
} }
// find if namespace has value parser
pm, ok := valueParserMap.Load(U.ToLowerNoSnake(l.Namespace))
if !ok {
return l, nil 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]()

View file

@ -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
}

View file

@ -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)
// }

View file

@ -8,11 +8,15 @@ import (
. "github.com/yusing/go-proxy/internal/utils/testing" . "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) { func TestNestedLabel(t *testing.T) {
mName := "middleware1" mName := "middleware1"
mAttr := "prop1" mAttr := "prop1"
v := "value1" 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()) ExpectNoError(t, err.Error())
sGot := ExpectType[*Label](t, pl.Value) sGot := ExpectType[*Label](t, pl.Value)
ExpectFalse(t, sGot == nil) ExpectFalse(t, sGot == nil)
@ -27,7 +31,7 @@ func TestApplyNestedLabel(t *testing.T) {
mName := "middleware1" mName := "middleware1"
mAttr := "prop1" mAttr := "prop1"
v := "value1" 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()) ExpectNoError(t, err.Error())
err = ApplyLabel(entry, pl) err = ApplyLabel(entry, pl)
ExpectNoError(t, err.Error()) ExpectNoError(t, err.Error())
@ -51,7 +55,7 @@ func TestApplyNestedLabelExisting(t *testing.T) {
entry.Middlewares[mName] = make(U.SerializedObject) entry.Middlewares[mName] = make(U.SerializedObject)
entry.Middlewares[mName][checkAttr] = checkV 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()) ExpectNoError(t, err.Error())
err = ApplyLabel(entry, pl) err = ApplyLabel(entry, pl)
ExpectNoError(t, err.Error()) ExpectNoError(t, err.Error())
@ -76,7 +80,7 @@ func TestApplyNestedLabelNoAttr(t *testing.T) {
entry.Middlewares = make(NestedLabelMap) entry.Middlewares = make(NestedLabelMap)
entry.Middlewares[mName] = make(U.SerializedObject) 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()) ExpectNoError(t, err.Error())
err = ApplyLabel(entry, pl) err = ApplyLabel(entry, pl)
ExpectNoError(t, err.Error()) ExpectNoError(t, err.Error())

View file

@ -3,6 +3,8 @@ package docker
const ( const (
WildcardAlias = "*" WildcardAlias = "*"
NSProxy = "proxy"
LabelAliases = NSProxy + ".aliases" LabelAliases = NSProxy + ".aliases"
LabelExclude = NSProxy + ".exclude" LabelExclude = NSProxy + ".exclude"
LabelIdleTimeout = NSProxy + ".idle_timeout" LabelIdleTimeout = NSProxy + ".idle_timeout"

View file

@ -4,7 +4,6 @@ import (
"net" "net"
"net/http" "net/http"
D "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/types" "github.com/yusing/go-proxy/internal/types"
F "github.com/yusing/go-proxy/internal/utils/functional" F "github.com/yusing/go-proxy/internal/utils/functional"
@ -24,13 +23,7 @@ type cidrWhitelistOpts struct {
} }
var CIDRWhiteList = &cidrWhitelist{ var CIDRWhiteList = &cidrWhitelist{
m: &Middleware{ m: &Middleware{withOptions: NewCIDRWhitelist},
labelParserMap: D.ValueParserMap{
"allow": D.YamlStringListParser,
"statusCode": D.IntParser,
},
withOptions: NewCIDRWhitelist,
},
} }
var cidrWhitelistDefaults = func() *cidrWhitelistOpts { var cidrWhitelistDefaults = func() *cidrWhitelistOpts {

View file

@ -30,9 +30,7 @@ var (
) )
var CloudflareRealIP = &realIP{ var CloudflareRealIP = &realIP{
m: &Middleware{ m: &Middleware{withOptions: NewCloudflareRealIP},
withOptions: NewCloudflareRealIP,
},
} }
func NewCloudflareRealIP(_ OptionsRaw) (*Middleware, E.NestedError) { func NewCloudflareRealIP(_ OptionsRaw) (*Middleware, E.NestedError) {

View file

@ -13,7 +13,6 @@ import (
"strings" "strings"
"time" "time"
D "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
gpHTTP "github.com/yusing/go-proxy/internal/net/http" gpHTTP "github.com/yusing/go-proxy/internal/net/http"
) )
@ -33,17 +32,9 @@ type (
} }
) )
var ForwardAuth = func() *forwardAuth { var ForwardAuth = &forwardAuth{
fa := new(forwardAuth) m: &Middleware{withOptions: NewForwardAuthfunc},
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
}()
func NewForwardAuthfunc(optsRaw OptionsRaw) (*Middleware, E.NestedError) { func NewForwardAuthfunc(optsRaw OptionsRaw) (*Middleware, E.NestedError) {
faWithOpts := new(forwardAuth) faWithOpts := new(forwardAuth)

View file

@ -5,7 +5,6 @@ import (
"errors" "errors"
"net/http" "net/http"
D "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
gpHTTP "github.com/yusing/go-proxy/internal/net/http" gpHTTP "github.com/yusing/go-proxy/internal/net/http"
U "github.com/yusing/go-proxy/internal/utils" U "github.com/yusing/go-proxy/internal/utils"
@ -37,7 +36,6 @@ type (
modifyResponse ModifyResponseFunc // runs after ReverseProxy.ModifyResponse modifyResponse ModifyResponseFunc // runs after ReverseProxy.ModifyResponse
withOptions CloneWithOptFunc withOptions CloneWithOptFunc
labelParserMap D.ValueParserMap
impl any impl any
parent *Middleware parent *Middleware
@ -92,7 +90,7 @@ func (m *Middleware) WithOptionsClone(optsRaw OptionsRaw) (*Middleware, E.Nested
m.name, m.name,
m.before, m.before,
m.modifyResponse, m.modifyResponse,
nil, nil, nil,
m.impl, m.impl,
m.parent, m.parent,
m.children, m.children,

View file

@ -8,7 +8,6 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
D "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
U "github.com/yusing/go-proxy/internal/utils" U "github.com/yusing/go-proxy/internal/utils"
) )
@ -42,11 +41,6 @@ func init() {
names := make(map[*Middleware][]string) names := make(map[*Middleware][]string)
for name, m := range middlewares { for name, m := range middlewares {
names[m] = append(names[m], http.CanonicalHeaderKey(name)) 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 { for m, names := range names {
if len(names) > 1 { if len(names) > 1 {

View file

@ -2,7 +2,6 @@ package middleware
import ( import (
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
D "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
) )
@ -19,17 +18,9 @@ type (
} }
) )
var ModifyRequest = func() *modifyRequest { var ModifyRequest = &modifyRequest{
mr := new(modifyRequest) m: &Middleware{withOptions: NewModifyRequest},
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
}()
func NewModifyRequest(optsRaw OptionsRaw) (*Middleware, E.NestedError) { func NewModifyRequest(optsRaw OptionsRaw) (*Middleware, E.NestedError) {
mr := new(modifyRequest) mr := new(modifyRequest)

View file

@ -4,7 +4,6 @@ import (
"net/http" "net/http"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
D "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
) )
@ -21,17 +20,9 @@ type (
} }
) )
var ModifyResponse = func() (mr *modifyResponse) { var ModifyResponse = &modifyResponse{
mr = new(modifyResponse) m: &Middleware{withOptions: NewModifyResponse},
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
}()
func NewModifyResponse(optsRaw OptionsRaw) (*Middleware, E.NestedError) { func NewModifyResponse(optsRaw OptionsRaw) (*Middleware, E.NestedError) {
mr := new(modifyResponse) mr := new(modifyResponse)

View file

@ -3,7 +3,6 @@ package middleware
import ( import (
"net" "net"
D "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/types" "github.com/yusing/go-proxy/internal/types"
) )
@ -32,13 +31,7 @@ type realIPOpts struct {
} }
var RealIP = &realIP{ var RealIP = &realIP{
m: &Middleware{ m: &Middleware{withOptions: NewRealIP},
labelParserMap: D.ValueParserMap{
"from": D.YamlStringListParser,
"recursive": D.BoolParser,
},
withOptions: NewRealIP,
},
} }
var realIPOptsDefault = func() *realIPOpts { var realIPOptsDefault = func() *realIPOpts {

View file

@ -5,7 +5,9 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"strconv"
"strings" "strings"
"unicode"
"github.com/santhosh-tekuri/jsonschema" "github.com/santhosh-tekuri/jsonschema"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
@ -210,17 +212,16 @@ func Convert(src reflect.Value, dst reflect.Value) E.NestedError {
switch { switch {
case srcT.AssignableTo(dstT): case srcT.AssignableTo(dstT):
dst.Set(src) dst.Set(src)
return nil
case srcT.ConvertibleTo(dstT): case srcT.ConvertibleTo(dstT):
dst.Set(src.Convert(dstT)) dst.Set(src.Convert(dstT))
return nil
case srcT.Kind() == reflect.Map: case srcT.Kind() == reflect.Map:
obj, ok := src.Interface().(SerializedObject) obj, ok := src.Interface().(SerializedObject)
if !ok { if !ok {
return E.TypeMismatch[SerializedObject](src.Interface()) return E.TypeMismatch[SerializedObject](src.Interface())
} }
err := Deserialize(obj, dst.Addr().Interface()) return Deserialize(obj, dst.Addr().Interface())
if err != nil {
return err
}
case srcT.Kind() == reflect.Slice: case srcT.Kind() == reflect.Slice:
if dstT.Kind() != reflect.Slice { if dstT.Kind() != reflect.Slice {
return E.TypeError("slice", srcT, dstT) return E.TypeError("slice", srcT, dstT)
@ -237,7 +238,13 @@ func Convert(src reflect.Value, dst reflect.Value) E.NestedError {
i++ i++
} }
dst.Set(newSlice) dst.Set(newSlice)
default: return nil
case src.Kind() == reflect.String:
if convertible, err := ConvertString(src.String(), dst); convertible {
return err
}
}
var converter Converter var converter Converter
var ok bool var ok bool
// check if (*T).Convertor is implemented // check if (*T).Convertor is implemented
@ -261,7 +268,82 @@ func Convert(src reflect.Value, dst reflect.Value) E.NestedError {
return nil return nil
} }
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 { func DeserializeJson(j map[string]string, target any) E.NestedError {

View file

@ -4,6 +4,8 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
E "github.com/yusing/go-proxy/internal/error"
) )
func CommaSeperatedList(s string) []string { func CommaSeperatedList(s string) []string {
@ -14,6 +16,10 @@ func CommaSeperatedList(s string) []string {
return res return res
} }
func IntParser(value string) (int, E.NestedError) {
return E.Check(strconv.Atoi(value))
}
func ExtractPort(fullURL string) (int, error) { func ExtractPort(fullURL string) (int, error) {
url, err := url.Parse(fullURL) url, err := url.Parse(fullURL)
if err != nil { if err != nil {