diff --git a/docs/docker.md b/docs/docker.md index 8304b68..fe14cba 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -62,20 +62,23 @@ ## Labels +**Parts surrounded by `[]` are optional** + ### Syntax -| Label | Description | Example | Default | Accepted values | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | --------------------------- | ------------------------------------------------------------------------- | -| `proxy.aliases` | comma separated aliases for subdomain and label matching | `gitlab,gitlab-reg,gitlab-ssh` | `container_name` | any | -| `proxy.exclude` | to be excluded from `go-proxy` | | false | boolean | -| `proxy.idle_timeout` | time for idle (no traffic) before put it into sleep **(http/s only)**
_**NOTE: idlewatcher will only be enabled containers that has non-empty `idle_timeout`**_ | `1h` | empty or `0` **(disabled)** | `number[unit]...`, e.g. `1m30s` | -| `proxy.wake_timeout` | time to wait for target site to be ready | | `30s` | `number[unit]...` | -| `proxy.stop_method` | method to stop after `idle_timeout` | | `stop` | `stop`, `pause`, `kill` | -| `proxy.stop_timeout` | time to wait for stop command | | `10s` | `number[unit]...` | -| `proxy.stop_signal` | signal sent to container for `stop` and `kill` methods | | docker's default | `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT` and those without **SIG** prefix | -| `proxy..` | set field for specific alias | `proxy.gitlab-ssh.scheme` | N/A | N/A | -| `proxy.#.` | set field for specific alias at index (starting from **1**) | `proxy.#3.port` | N/A | N/A | -| `proxy.*.` | set field for all aliases | `proxy.*.set_headers` | N/A | N/A | +| Label | Description | Example | Default | Accepted values | +| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------- | +| `proxy.aliases` | comma separated aliases for subdomain and label matching | `gitlab,gitlab-reg,gitlab-ssh` | `container_name` | any | +| `proxy.exclude` | to be excluded from `go-proxy` | | false | boolean | +| `proxy.idle_timeout` | time for idle (no traffic) before put it into sleep **(http/s only)**
_**NOTE: idlewatcher will only be enabled containers that has non-empty `idle_timeout`**_ | `1h` | empty or `0` **(disabled)** | `number[unit]...`, e.g. `1m30s` | +| `proxy.wake_timeout` | time to wait for target site to be ready | | `30s` | `number[unit]...` | +| `proxy.stop_method` | method to stop after `idle_timeout` | | `stop` | `stop`, `pause`, `kill` | +| `proxy.stop_timeout` | time to wait for stop command | | `10s` | `number[unit]...` | +| `proxy.stop_signal` | signal sent to container for `stop` and `kill` methods | | docker's default | `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT` and those without **SIG** prefix | +| `proxy..` | set field for specific alias | `proxy.gitlab-ssh.scheme` | N/A | N/A | +| `proxy.#.` | set field for specific alias at index (starting from **1**) | `proxy.#3.port` | N/A | N/A | +| `proxy.*.` | set field for all aliases | `proxy.*.set_headers` | N/A | N/A | +| `proxy.?.middlewares.[.]` | enable and set field for specific middleware | **?** here means `` / `$` / `*`
  • `proxy.#1.middlewares.modify_request.set_headers`
  • `proxy.*.middlewares.modify_response.hide_headers`
  • `proxy.app1.middlewares.redirect_http`
| N/A | Middleware specific
See [middlewares.md](middlewares.md) for more | ### Fields @@ -87,8 +90,7 @@ | `port` | proxy port **(tcp/udp)** | `0:first_port` | `x:y`
  • **x**: port for `go-proxy` to listen on.
    **x** can be 0, which means listen on a random port
  • **y**: port or [_service name_](../src/common/constants.go#L55) of target container
| | `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)) | -| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[2](#key-value-mapping-example) of header-value pairs | -| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[1](#list-example) of headers | + [🔼Back to top](#table-of-content) @@ -101,12 +103,9 @@ services: nginx: ... labels: - # values from duplicated header keys will be combined - proxy.nginx.set_headers: | # remember to add the '|' + proxy.nginx.middlewares.modify_request.set_headers: | # remember to add the '|' X-Custom-Header1: value1, value2 - X-Custom-Header2: value3 - X-Custom-Header2: value4 - # X-Custom-Header2 will be "value3, value4" + X-Custom-Header2: value3, value4 ``` File Provider @@ -114,10 +113,11 @@ File Provider ```yaml service_a: host: service_a.internal - set_headers: - # do not duplicate header keys, as it is not allowed in YAML - X-Custom-Header1: value1, value2 - X-Custom-Header2: value3 + middlewares: + modify_request: + set_headers: + X-Custom-Header1: value1, value2 + X-Custom-Header2: value3 ``` [🔼Back to top](#table-of-content) @@ -134,12 +134,12 @@ services: proxy.nginx.path_patterns: | # remember to add the '|' - GET / - POST /auth - proxy.nginx.hide_headers: | # remember to add the '|' + proxy.nginx.middlewares.modify_request.hide_headers: | # remember to add the '|' - X-Custom-Header1 - X-Custom-Header2 ``` -File Provider +Include file ```yaml service_a: @@ -147,9 +147,11 @@ service_a: path_patterns: - GET / - POST /auth - hide_headers: - - X-Custom-Header1 - - X-Custom-Header2 + middlewares: + modify_request: + hide_headers: + - X-Custom-Header1 + - X-Custom-Header2 ``` [🔼Back to top](#table-of-content) @@ -209,10 +211,10 @@ services: restart: unless-stopped labels: - proxy.aliases=adg,adg-dns,adg-setup - - proxy.$1.port=80 - - proxy.$2.scheme=udp - - proxy.$2.port=20000:dns - - proxy.$3.port=3000 + - proxy.#1.port=80 + - proxy.#2.scheme=udp + - proxy.#2.port=20000:dns + - proxy.#3.port=3000 volumes: - adg-work:/opt/adguardhome/work - adg-conf:/opt/adguardhome/conf @@ -245,8 +247,8 @@ services: labels: - proxy.aliases=pal1,pal2 - proxy.*.scheme=udp - - proxy.$1.port=20002:8211 - - proxy.$2.port=20003:27015 + - proxy.#1.port=20002:8211 + - proxy.#2.port=20003:27015 environment: ... volumes: - palworld:/palworld diff --git a/docs/middlewares.md b/docs/middlewares.md new file mode 100644 index 0000000..6a1fbb4 --- /dev/null +++ b/docs/middlewares.md @@ -0,0 +1,245 @@ +# Middlewares + +## Table of content + + + +- [Middlewares](#middlewares) + - [Table of content](#table-of-content) + - [Available middlewares](#available-middlewares) + - [Redirect http](#redirect-http) + - [Modify request or response](#modify-request-or-response) + - [Set headers](#set-headers) + - [Add headers](#add-headers) + - [Hide headers](#hide-headers) + - [X-Forwarded-\* Headers](#x-forwarded--headers) + - [Add X-Forwarded-\*](#add-x-forwarded-) + - [Set X-Forwarded-\*](#set-x-forwarded-) + - [Forward Authorization header (experimental)](#forward-authorization-header-experimental) + - [Examples](#examples) + - [Authentik](#authentik) + + + +## Available middlewares + +### Redirect http + +Redirect http requests to https + +```yaml +# docker labels +proxy.app1.middlewares.redirect_http: + +# include file +app1: + middlewares: + redirect_http: +``` + +nginx equivalent: +```nginx +server { + listen 80; + server_name domain.tld; + return 301 https://$host$request_uri; +} +``` + +[🔼Back to top](#table-of-content) + +### Modify request or response + +```yaml +# docker labels +proxy.app1.middlewares.modify_request.field: +proxy.app1.middlewares.modify_response.field: + +# include file +app1: + middlewares: + modify_request: + field: + modify_response: + field: +``` + +#### Set headers + +```yaml +# docker labels +proxy.app1.middlewares.modify_request.set_headers: | + X-Custom-Header1: value1, value2 + X-Custom-Header2: value3 + +# include file +app1: + middlewares: + modify_request: + set_headers: + X-Custom-Header1: value1, value2 + X-Custom-Header2: value3 +``` + +nginx equivalent: +```nginx +location / { + add_header X-Custom-Header1 value1, value2; + add_header X-Custom-Header2 value3; +} +``` + +#### Add headers + +```yaml +# docker labels +proxy.app1.middlewares.modify_request.add_headers: | + X-Custom-Header1: value1, value2 + X-Custom-Header2: value3 + +# include file +app1: + middlewares: + modify_request: + add_headers: + X-Custom-Header1: value1, value2 + X-Custom-Header2: value3 +``` + +nginx equivalent: +```nginx +location / { + more_set_headers "X-Custom-Header1: value1, value2"; + more_set_headers "X-Custom-Header2: value3"; +} +``` + +#### Hide headers + +```yaml +# docker labels +proxy.app1.middlewares.modify_request.hide_headers: | + - X-Custom-Header1 + - X-Custom-Header2 + +# include file +app1: + middlewares: + modify_request: + hide_headers: + - X-Custom-Header1 + - X-Custom-Header2 +``` + +nginx equivalent: +```nginx +location / { + more_clear_headers "X-Custom-Header1"; + more_clear_headers "X-Custom-Header2"; +} +``` + +### X-Forwarded-* Headers + +#### Add X-Forwarded-* + +Append `X-Forwarded-*` headers to existing headers + +```yaml +# docker labels +proxy.app1.middlewares.modify_request.add_x_forwarded: + +# include file +app1: + middlewares: + modify_request: + add_x_forwarded: +``` + +#### Set X-Forwarded-* + +Replace existing `X-Forwarded-*` headers with `go-proxy` provided headers + +```yaml +# docker labels +proxy.app1.middlewares.modify_request.set_x_forwarded: + +# include file +app1: + middlewares: + modify_request: + set_x_forwarded: +``` + +### Forward Authorization header (experimental) + +Fields: +- `address`: authentication provider URL _(required)_ +- `trust_forward_header`: whether to trust `X-Forwarded-*` headers from upstream proxies _(default: `false`)_ +- `auth_response_headers`: list of headers to copy from auth response _(default: empty)_ +- `add_auth_cookies_to_response`: list of cookies to add to response _(default: empty)_ + +```yaml +# docker labels +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 +proxy.app1.middlewares.forward_auth.add_auth_cookies_to_response: | + - uid + - session_id + +# include file +app1: + middlewares: + forward_authorization: + address: https://auth.example.com + trust_forward_header: true + auth_response_headers: + - X-Auth-Token + - X-Auth-User + add_auth_cookies_to_response: + - uid + - session_id +``` + +Traefik equivalent: +```yaml +# docker labels +traefik.http.middlewares.authentik.forwardauth.address: https://auth.example.com +traefik.http.middlewares.authentik.forwardauth.trustForwardHeader: true +traefik.http.middlewares.authentik.forwardauth.authResponseHeaders: X-Auth-Token, X-Auth-User +traefik.http.middlewares.authentik.forwardauth.addAuthCookiesToResponse: uid, session_id + +# standalone +http: + middlewares: + forwardAuth: + address: https://auth.example.com + trustForwardHeader: true + authResponseHeaders: + - X-Auth-Token + - X-Auth-User + addAuthCookiesToResponse: + - uid + - session_id +``` + +## Examples + +### Authentik + +```yaml +# docker compose +services: + ... + server: + ... + container_name: authentik + labels: + proxy.authentik.middlewares.redirect_http: + proxy.authentik.middlewares.set_x_forwarded: + proxy.authentik.middlewares.modify_request.add_headers: | + Strict-Transport-Security: "max-age=63072000" always +``` \ No newline at end of file diff --git a/src/common/ports.go b/src/common/ports.go index a4f3fc1..7816355 100644 --- a/src/common/ports.go +++ b/src/common/ports.go @@ -59,8 +59,8 @@ var ( "nginx-proxy-manager": 81, "open-webui": 8080, "plex": 32400, - "portainer": 9000, - "portainer-ce": 9000, + "portainer-be": 9443, + "portainer-ce": 9443, "prometheus": 9090, "prowlarr": 9696, "radarr": 7878, diff --git a/src/docker/label.go b/src/docker/label.go index edac8b9..39b288f 100644 --- a/src/docker/label.go +++ b/src/docker/label.go @@ -108,12 +108,12 @@ func ParseLabel(label string, value string) (*Label, E.NestedError) { } // find if namespace has value parser - pm, ok := valueParserMap.Load(l.Namespace) + pm, ok := valueParserMap.Load(U.ToLowerNoSnake(l.Namespace)) if !ok { return l, nil } // find if attribute has value parser - p, ok := pm[l.Attribute] + p, ok := pm[U.ToLowerNoSnake(l.Attribute)] if !ok { return l, nil } @@ -127,7 +127,11 @@ func ParseLabel(label string, value string) (*Label, E.NestedError) { } func RegisterNamespace(namespace string, pm ValueParserMap) { - valueParserMap.Store(namespace, pm) + 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 { diff --git a/src/docker/label_parser.go b/src/docker/label_parser.go index def4b07..91b1c34 100644 --- a/src/docker/label_parser.go +++ b/src/docker/label_parser.go @@ -49,7 +49,7 @@ func YamlLikeMappingParser(allowDuplicate bool) func(string) (any, E.NestedError for _, line := range lines { parts := strings.SplitN(line, ":", 2) if len(parts) != 2 { - return nil, E.Invalid("syntax", line) + return nil, E.Invalid("syntax", line).With("too many colons") } key := strings.TrimSpace(parts[0]) val := strings.TrimSpace(parts[1]) diff --git a/src/route/middleware/forward_auth.go b/src/route/middleware/forward_auth.go index 0276da0..f7e80e7 100644 --- a/src/route/middleware/forward_auth.go +++ b/src/route/middleware/forward_auth.go @@ -74,7 +74,7 @@ func newForwardAuth() (fa *forwardAuth) { } faWithOpts.m = &Middleware{ impl: faWithOpts, - before: fa.forward, + before: faWithOpts.forward, } err := U.Deserialize(optsRaw, faWithOpts.forwardAuthOpts) diff --git a/src/utils/serialization.go b/src/utils/serialization.go index c4cd310..bfff55e 100644 --- a/src/utils/serialization.go +++ b/src/utils/serialization.go @@ -108,13 +108,13 @@ func Serialize(data any) (SerializedObject, E.NestedError) { func Deserialize(src SerializedObject, target any) E.NestedError { // convert data fields to lower no-snake - // convert target fields to lower + // convert target fields to lower no-snake // then check if the field of data is in the target mapping := make(map[string]string) t := reflect.TypeOf(target).Elem() for i := 0; i < t.NumField(); i++ { field := t.Field(i) - snakeCaseField := strings.ToLower(field.Name) + snakeCaseField := ToLowerNoSnake(field.Name) mapping[snakeCaseField] = field.Name } tValue := reflect.ValueOf(target) @@ -122,7 +122,7 @@ func Deserialize(src SerializedObject, target any) E.NestedError { return E.Invalid("value", "nil") } for k, v := range src { - kCleaned := toLowerNoSnake(k) + kCleaned := ToLowerNoSnake(k) if fieldName, ok := mapping[kCleaned]; ok { prop := reflect.ValueOf(target).Elem().FieldByName(fieldName) propType := prop.Type() @@ -175,7 +175,7 @@ func DeserializeJson(j map[string]string, target any) E.NestedError { return E.From(json.Unmarshal(data, target)) } -func toLowerNoSnake(s string) string { +func ToLowerNoSnake(s string) string { return strings.ToLower(strings.ReplaceAll(s, "_", "")) }