From 2e7ba51521c854061960f10172088b8201a6fb8d Mon Sep 17 00:00:00 2001 From: yusing Date: Mon, 16 Sep 2024 07:21:45 +0800 Subject: [PATCH] v0.5: (BREAKING) new syntax for set_headers and hide_headers, updated label parser, error.Nil().String() will now return 'nil', better readme --- Dockerfile | 2 +- README.md | 22 +- docs/docker.md | 207 ++++++++++-------- go.work | 4 +- .../{proxy_label.go => label_parser.go} | 15 +- ...oxy_label_test.go => label_parser_test.go} | 102 ++++----- src/error/error.go | 7 +- src/error/error_test.go | 10 + src/go.mod | 4 +- 9 files changed, 197 insertions(+), 176 deletions(-) rename src/docker/{proxy_label.go => label_parser.go} (77%) rename src/docker/{proxy_label_test.go => label_parser_test.go} (58%) diff --git a/Dockerfile b/Dockerfile index 5415e705..ba05a869 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22.6-alpine AS builder +FROM golang:1.23.1-alpine AS builder COPY src /src ENV GOCACHE=/root/.cache/go-build WORKDIR /src diff --git a/README.md b/README.md index 7386149e..247d4a7a 100755 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr - [go-proxy](#go-proxy) - [Key Points](#key-points) - [Getting Started](#getting-started) - - [Commands](#commands) + - [Commands line arguments](#commands-line-arguments) - [Environment variables](#environment-variables) - [Use JSON Schema in VSCode](#use-json-schema-in-vscode) - [Config File](#config-file) @@ -45,20 +45,22 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr [🔼Back to top](#table-of-content) -### Commands +### Commands line arguments -- `go-proxy` start proxy server -- `go-proxy validate` validate config and exit -- `go-proxy reload` trigger a force reload of config +| Argument | Description | +| ---------- | -------------------------------- | +| empty | start proxy server | +| `validate` | validate config and exit | +| `reload` | trigger a force reload of config | -**For docker containers, run `docker exec -it go-proxy /app/go-proxy `** +**run with `docker exec /app/go-proxy `** ### Environment variables -Booleans: - -- `GOPROXY_DEBUG` enable debug behaviors -- `GOPROXY_NO_SCHEMA_VALIDATION`: disable schema validation **(useful for testing new DNS Challenge providers)** +| Environment Variable | Description | Default | Values | +| ------------------------------ | ------------------------- | ------- | ------- | +| `GOPROXY_NO_SCHEMA_VALIDATION` | disable schema validation | `false` | boolean | +| `GOPROXY_DEBUG` | enable debug behaviors | `false` | boolean | ### Use JSON Schema in VSCode diff --git a/docs/docker.md b/docs/docker.md index 6621003b..1db8a7da 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,126 +1,161 @@ -# Docker container guide +# Docker compose guide ## Table of content -- [Docker container guide](#docker-container-guide) +- [Docker compose guide](#docker-compose-guide) - [Table of content](#table-of-content) - [Setup](#setup) - [Labels](#labels) + - [Syntax](#syntax) + - [Fields](#fields) + - [Key-value mapping example](#key-value-mapping-example) + - [List example](#list-example) - [Troubleshooting](#troubleshooting) - [Docker compose examples](#docker-compose-examples) - - [Local docker provider in bridge network](#local-docker-provider-in-bridge-network) - - [Proxy setup](#proxy-setup) - [Services URLs for above examples](#services-urls-for-above-examples) ## Setup -1. Install `wget` if not already +1. Install `wget` if not already -2. Run setup script + - Ubuntu based: `sudo apt install -y wget` + - Fedora based: `sudo yum install -y wget` + - Arch based: `sudo pacman -Sy wget` - `bash <(wget -qO- https://github.com/yusing/go-proxy/raw/main/setup-docker.sh)` +2. Run setup script - What it does: + `bash <(wget -qO- https://github.com/yusing/go-proxy/raw/main/setup-docker.sh)` - - Create required directories - - Setup `config.yml` and `compose.yml` + It will setup folder structure and required config files -3. Verify folder structure and then `cd go-proxy` +3. Verify folder structure and then `cd go-proxy` - ```plain - go-proxy - ├── certs - ├── compose.yml - └── config - ├── config.yml - └── providers.yml - ``` + ```plain + go-proxy + ├── certs + ├── compose.yml + └── config + ├── config.yml + └── providers.yml + ``` -4. Enable HTTPs _(optional)_ +4. Enable HTTPs _(optional)_ - - To use autocert feature + Mount a folder (to store obtained certs) or (containing existing cert) - - completing `autocert` section in `config/config.yml` - - mount `certs/` to `/app/certs` to store obtained certs + ```yaml + services: + go-proxy: + ... + volumes: + - ./certs:/app/certs + ``` - - To use existing certificate + To use **autocert**, complete that section in `config.yml`, e.g. - mount your wildcard (`*.y.z`) SSL cert + ```yaml + autocert: + email: john.doe@x.y.z # ACME Email + domains: # a list of domains for cert registration + - x.y.z + provider: cloudflare + options: + - auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token + ``` - - cert / chain / fullchain -> `/app/certs/cert.crt` - - private key -> `/app/certs/priv.key` + To use **existing certificate**, set path for cert and key in `config.yml`, e.g. -5. Modify `compose.yml` fit your needs + ```yaml + autocert: + cert_path: /app/certs/cert.crt + key_path: /app/certs/priv.key + ``` - Add networks to make sure it is in the same network with other containers, or make sure `proxy..host` is reachable +5. Modify `compose.yml` to fit your needs -6. Run `docker compose up -d` to start the container +6. Run `docker compose up -d` to start the container -7. Start editing config files in `http://:8080` +7. Navigate to Web panel `http://gp.yourdomain.com` and edit proxy config [🔼Back to top](#table-of-content) ## Labels -- `proxy.aliases`: comma separated aliases for subdomain matching +### Syntax - - default: container name +| Label | Description | +| ----------------------- | -------------------------------------------------------- | +| `proxy.aliases` | comma separated aliases for subdomain and label matching | +| `proxy..` | set field for specific alias | +| `proxy.*.` | set field for all aliases | -- `proxy.*.`: wildcard label for all aliases +### Fields -_Labels below should have a **`proxy..`** prefix._ +| Field | Description | Default | Allowed Values / Syntax | +| --------------------- | ---------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `scheme` | proxy protocol |
  • `http` for numeric port
  • `tcp` for `x:y` port
| `http`, `https`, `tcp`, `udp` | +| `host` | proxy host | `container_name` | IP address, hostname | +| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `0 - 65535` | +| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y`
  • x: port for `go-proxy` to listen on
  • 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` | proxy path | empty | **(http/s only)** string | +| `path_mode` | path handling **(http/s only)** | empty | empty, `forward` | +| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[1](#1-key-value-mapping-example) | +| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[2](#2-list-example) | -_i.e. `proxy.nginx.scheme: http`_ +#### Key-value mapping example -- `scheme`: proxy protocol - - default: - - if `port` is like `x:y`: `tcp` - - if `port` is a number: `http` - - allowed: `http`, `https`, `tcp`, `udp` -- `host`: proxy host - - default: `container_name` - - allowed: IP address, hostname -- `port`: proxy port - - default: first port in `ports:` - - `http(s)`: number in range og `0 - 65535` - - `tcp`, `udp`: `x:y` - - `x`: port for `go-proxy` to listen on - - `y`: port, or _service name_ of target container - see [constants.go:14 for _service names_](../src/common/constants.go#L74) -- `no_tls_verify`: whether skip tls verify when scheme is https - - default: `false` -- `path`: proxy path _(http(s) proxy only)_ - - default: empty -- `path_mode`: mode for path handling +Docker Compose - - default: empty - - allowed: empty, `forward` +```yaml +services: + nginx: + ... + labels: + # values from duplicated header keys will be combined + proxy.nginx.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" +``` - - `empty`: remove path prefix from URL when proxying - 1. apps.y.z/webdav -> webdav:80 - 2. apps.y.z./webdav/path/to/file -> webdav:80/path/to/file - - `forward`: path remain unchanged - 1. apps.y.z/webdav -> webdav:80/webdav - 2. apps.y.z./webdav/path/to/file -> webdav:80/webdav/path/to/file +File Provider -- `set_headers`: a list of header to set, (key:value, one by line) +```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 +``` - Duplicated keys will be treated as multiple-value headers +#### List example - ```yaml - labels: - proxy.app.set_headers: | - X-Custom-Header1: value1 - X-Custom-Header1: value2 - X-Custom-Header2: value2 - ``` +Docker Compose -- `hide_headers`: comma seperated list of headers to hide +```yaml +services: + nginx: + ... + labels: + proxy.nginx.hide_headers: | # remember to add the '|' + - X-Custom-Header1 + - X-Custom-Header2 +``` -- `load_balance`: enable load balance - - allowed: `1`, `true` +File Provider + +```yaml +service_a: + host: service_a.internal + hide_headers: + - X-Custom-Header1 + - X-Custom-Header2 +``` [🔼Back to top](#table-of-content) @@ -146,8 +181,6 @@ _i.e. `proxy.nginx.scheme: http`_ ## Docker compose examples -### Local docker provider in bridge network - ```yaml volumes: adg-work: @@ -220,24 +253,6 @@ services: [🔼Back to top](#table-of-content) -#### Proxy setup - -```yaml -go-proxy: - image: ghcr.io/yusing/go-proxy - container_name: go-proxy - restart: always - network_mode: host - volumes: - - ./config:/app/config - - /var/run/docker.sock:/var/run/docker.sock:ro - labels: - - proxy.aliases=gp - - proxy.gp.port=8080 -``` - -[🔼Back to top](#table-of-content) - ### Services URLs for above examples - `gp.yourdomain.com`: go-proxy web panel diff --git a/go.work b/go.work index 95836059..1aa0ac63 100644 --- a/go.work +++ b/go.work @@ -1,5 +1,5 @@ -go 1.22.0 +go 1.22 -toolchain go1.22.6 +toolchain go1.23.1 use ./src diff --git a/src/docker/proxy_label.go b/src/docker/label_parser.go similarity index 77% rename from src/docker/proxy_label.go rename to src/docker/label_parser.go index 083953d0..7a102270 100644 --- a/src/docker/proxy_label.go +++ b/src/docker/label_parser.go @@ -5,8 +5,15 @@ import ( "strings" E "github.com/yusing/go-proxy/error" + "gopkg.in/yaml.v3" ) +func yamlParser[T any](value string) (any, E.NestedError) { + var data T + err := E.From(yaml.Unmarshal([]byte(value), &data)) + return data, err +} + func setHeadersParser(value string) (any, E.NestedError) { value = strings.TrimSpace(value) lines := strings.Split(value, "\n") @@ -17,8 +24,10 @@ func setHeadersParser(value string) (any, E.NestedError) { return nil, E.Invalid("set header statement", line) } key := strings.TrimSpace(parts[0]) - val := strings.TrimSpace(parts[1]) - h.Add(key, val) + vals := strings.Split(parts[1], ",") + for i := range vals { + h.Add(key, strings.TrimSpace(vals[i])) + } } return h, E.Nil() } @@ -48,7 +57,7 @@ var _ = func() int { RegisterNamespace(NSProxy, ValueParserMap{ "aliases": commaSepParser, "set_headers": setHeadersParser, - "hide_headers": commaSepParser, + "hide_headers": yamlParser[[]string], "no_tls_verify": boolParser, }) return 0 diff --git a/src/docker/proxy_label_test.go b/src/docker/label_parser_test.go similarity index 58% rename from src/docker/proxy_label_test.go rename to src/docker/label_parser_test.go index c87c091b..266049a4 100644 --- a/src/docker/proxy_label_test.go +++ b/src/docker/label_parser_test.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "reflect" + "strings" "testing" E "github.com/yusing/go-proxy/error" @@ -33,27 +34,17 @@ func TestHomePageLabel(t *testing.T) { } func TestStringProxyLabel(t *testing.T) { - alias := "foo" - field := "ip" v := "bar" - pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v) + pl, err := ParseLabel(makeLabel(NSProxy, "foo", "ip"), v) if err.IsNotNil() { t.Errorf("expected err=nil, got %s", err.Error()) } - if pl.Target != alias { - t.Errorf("expected alias=%s, got %s", alias, pl.Target) - } - if pl.Attribute != field { - t.Errorf("expected field=%s, got %s", field, pl.Target) - } if pl.Value != v { t.Errorf("expected value=%q, got %s", v, pl.Value) } } func TestBoolProxyLabelValid(t *testing.T) { - alias := "foo" - field := "no_tls_verify" tests := map[string]bool{ "true": true, "TRUE": true, @@ -66,16 +57,10 @@ func TestBoolProxyLabelValid(t *testing.T) { } for k, v := range tests { - pl, err := ParseLabel(makeLabel(NSProxy, alias, field), k) + pl, err := ParseLabel(makeLabel(NSProxy, "foo", "no_tls_verify"), k) if err.IsNotNil() { t.Errorf("expected err=nil, got %s", err.Error()) } - if pl.Target != alias { - t.Errorf("expected alias=%s, got %s", alias, pl.Target) - } - if pl.Attribute != field { - t.Errorf("expected field=%s, got %s", field, pl.Attribute) - } if pl.Value != v { t.Errorf("expected value=%v, got %v", v, pl.Value) } @@ -87,33 +72,26 @@ func TestBoolProxyLabelInvalid(t *testing.T) { field := "no_tls_verify" _, err := ParseLabel(makeLabel(NSProxy, alias, field), "invalid") if !err.Is(E.ErrInvalid) { - t.Errorf("expected err InvalidProxyLabel, got %v", reflect.TypeOf(err)) + t.Errorf("expected err InvalidProxyLabel, got %s", err.Error()) } } -func TestHeaderProxyLabelValid(t *testing.T) { - alias := "foo" - field := "set_headers" +func TestSetHeaderProxyLabelValid(t *testing.T) { v := ` - X-Custom-Header1: foo - X-Custom-Header1: bar - X-Custom-Header2: baz - ` +X-Custom-Header1: foo, bar +X-Custom-Header1: baz +X-Custom-Header2: boo` + v = strings.TrimPrefix(v, "\n") h := make(http.Header, 0) h.Set("X-Custom-Header1", "foo") h.Add("X-Custom-Header1", "bar") - h.Set("X-Custom-Header2", "baz") + h.Add("X-Custom-Header1", "baz") + h.Set("X-Custom-Header2", "boo") - pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v) + pl, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v) if err.IsNotNil() { t.Errorf("expected err=nil, got %s", err.Error()) } - if pl.Target != alias { - t.Errorf("expected alias=%s, got %s", alias, pl.Target) - } - if pl.Attribute != field { - t.Errorf("expected field=%s, got %s", field, pl.Attribute) - } hGot, ok := pl.Value.(http.Header) if !ok { t.Error("value is not http.Header") @@ -127,40 +105,52 @@ func TestHeaderProxyLabelValid(t *testing.T) { } } -func TestHeaderProxyLabelInvalid(t *testing.T) { - alias := "foo" - field := "set_headers" +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, alias, field), v) + _, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v) if !err.Is(E.ErrInvalid) { - t.Errorf("expected err InvalidProxyLabel for %q, got %v", v, err) + t.Errorf("expected invalid err for %q, got %s", v, err.Error()) } } } -func TestCommaSepProxyLabelSingle(t *testing.T) { - alias := "foo" - field := "hide_headers" - v := "X-Custom-Header1" - pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v) +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", "hide_headers"), v) if err.IsNotNil() { t.Errorf("expected err=nil, got %s", err.Error()) } - if pl.Target != alias { - t.Errorf("expected alias=%s, got %s", alias, pl.Target) + sGot, ok := pl.Value.([]string) + sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"} + if !ok { + t.Errorf("value is not []string, but %T", pl.Value) } - if pl.Attribute != field { - t.Errorf("expected field=%s, got %s", field, pl.Attribute) + if !reflect.DeepEqual(sGot, sWant) { + t.Errorf("expected %q, got %q", sWant, sGot) + } +} + +func TestCommaSepProxyLabelSingle(t *testing.T) { + v := "a" + pl, err := ParseLabel("proxy.aliases", v) + if err.IsNotNil() { + t.Errorf("expected err=nil, got %s", err.Error()) } sGot, ok := pl.Value.([]string) - sWant := []string{"X-Custom-Header1"} + sWant := []string{"a"} if !ok { - t.Error("value is not []string") + t.Errorf("value is not []string, but %T", pl.Value) } if !reflect.DeepEqual(sGot, sWant) { t.Errorf("expected %q, got %q", sWant, sGot) @@ -168,23 +158,15 @@ func TestCommaSepProxyLabelSingle(t *testing.T) { } func TestCommaSepProxyLabelMulti(t *testing.T) { - alias := "foo" - field := "hide_headers" v := "X-Custom-Header1, X-Custom-Header2,X-Custom-Header3" - pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v) + pl, err := ParseLabel("proxy.aliases", v) if err.IsNotNil() { t.Errorf("expected err=nil, got %s", err.Error()) } - if pl.Target != alias { - t.Errorf("expected alias=%s, got %s", alias, pl.Target) - } - if pl.Attribute != field { - t.Errorf("expected field=%s, got %s", field, pl.Attribute) - } sGot, ok := pl.Value.([]string) sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"} if !ok { - t.Error("value is not []string") + t.Errorf("value is not []string, but %T", pl.Value) } if !reflect.DeepEqual(sGot, sWant) { t.Errorf("expected %q, got %q", sWant, sGot) diff --git a/src/error/error.go b/src/error/error.go index 0aad026a..4acc6067 100644 --- a/src/error/error.go +++ b/src/error/error.go @@ -139,9 +139,12 @@ func (ne *NestedError) writeToSB(sb *strings.Builder, level int, prefix string) ne.writeIndents(sb, level) sb.WriteString(prefix) - if ne.err != nil { - sb.WriteString(ne.err.Error()) + if ne.IsNil() { + sb.WriteString("nil") + return } + + sb.WriteString(ne.err.Error()) if ne.subject != "" { if ne.err != nil { sb.WriteString(fmt.Sprintf(" for %q", ne.subject)) diff --git a/src/error/error_test.go b/src/error/error_test.go index f8a0e3b0..aa4760d4 100644 --- a/src/error/error_test.go +++ b/src/error/error_test.go @@ -19,6 +19,16 @@ func TestErrorIs(t *testing.T) { AssertEq(t, Invalid("foo", "bar").Is(ErrInvalid), true) AssertEq(t, Invalid("foo", "bar").Is(ErrFailure), false) + + AssertEq(t, Nil().Is(nil), true) + AssertEq(t, Nil().Is(ErrInvalid), false) + AssertEq(t, Invalid("foo", "bar").Is(nil), false) +} + +func TestNil(t *testing.T) { + AssertEq(t, Nil().IsNil(), true) + AssertEq(t, Nil().IsNotNil(), false) + AssertEq(t, Nil().Error(), "nil") } func TestErrorSimple(t *testing.T) { diff --git a/src/go.mod b/src/go.mod index a5562a6a..200d5966 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,8 +1,8 @@ module github.com/yusing/go-proxy -go 1.22.0 +go 1.22 -toolchain go1.22.6 +toolchain go1.23.1 require ( github.com/docker/cli v27.2.1+incompatible