diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 024b962..bfeb218 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -8,7 +8,13 @@ jobs: build_and_push: runs-on: ubuntu-latest steps: - - name: Build and Push Container to ghcr.io - uses: GlueOps/github-actions-build-push-containers@v0.3.7 - with: - tags: latest,${{ github.ref_name }} + - name: Set tags (latest) + if: "!endsWith(github.ref, '-dev')" + run: echo "::set-output name=tag::latest,${{ github.ref_name }}" + - name: Set tags (dev) + if: "endsWith(github.ref, '-dev')" + run: echo "::set-output name=tag::dev,${{ github.ref_name }}" + - name: Build and Push Container to ghcr.io + uses: GlueOps/github-actions-build-push-containers@v0.3.7 + with: + tags: ${{ steps.build_and_push.outputs.tag }} diff --git a/.gitignore b/.gitignore index 74b03fb..26ec954 100755 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ bin/ templates/codemirror/ logs/ -log/ \ No newline at end of file +log/ +.vscode/settings.json \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..cf34ab6 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,15 @@ +build-image: + image: docker + rules: + - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH + variables: + CI_REGISTRY_IMAGE: $CI_REGISTRY_IMAGE:latest + - if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH + variables: + CI_REGISTRY_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH + before_script: + - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin + script: + - echo building $CI_REGISTRY_IMAGE + - docker build --pull -t $CI_REGISTRY_IMAGE . + - docker push $CI_REGISTRY_IMAGE \ No newline at end of file diff --git a/.vscode/settings.example.json b/.vscode/settings.example.json new file mode 100644 index 0000000..a8acfd8 --- /dev/null +++ b/.vscode/settings.example.json @@ -0,0 +1,12 @@ +{ + "yaml.schemas": { + "https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [ + "config.example.yml", + "config.yml" + ], + "https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [ + "providers.example.yml", + "*.providers.yml" + ] + } +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100755 index 5fce611..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "go.inferGopath": false, - "yaml.schemas": { - // "https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [ - // "config.example.yml", - // "config.yml" - // ], - "https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [ - "providers.example.yml", - "*.providers.yml" - ], - "file:///config/workspace/go-proxy/schema/config.schema.json": [ - "file:///config/workspace/go-proxy/config.example.yml" - ] - } -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c9f7251..c0bc13e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ RUN apk add --no-cache unzip wget make COPY Makefile . RUN make setup-codemirror -FROM golang:1.22.1-alpine as builder +FROM golang:1.22.2-alpine as builder COPY src/ /src COPY go.mod go.sum /src/go-proxy WORKDIR /src/go-proxy diff --git a/Makefile b/Makefile index 46b6f02..896d74d 100755 --- a/Makefile +++ b/Makefile @@ -18,12 +18,14 @@ build: mkdir -p bin CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy src/go-proxy/*.go +test: + go test src/go-proxy/*.go + up: - docker compose up -d --build app + docker compose up -d restart: - docker kill go-proxy - docker compose up -d app + docker compose restart -t 0 logs: tail -f log/go-proxy.log @@ -31,6 +33,12 @@ logs: get: go get -d -u ./src/go-proxy +repush: + git reset --soft HEAD^ + git add -A + git commit -m "repush" + git push gitlab dev --force + udp-server: docker run -it --rm \ -p 9999:9999/udp \ diff --git a/README.md b/README.md index e66c60f..90b785a 100755 --- a/README.md +++ b/README.md @@ -6,35 +6,29 @@ In the examples domain `x.y.z` is used, replace them with your domain ## Table of content -- [go-proxy](#go-proxy) - - [Table of content](#table-of-content) - - [Key Points](#key-points) - - [How to use](#how-to-use) - - [Tested Services](#tested-services) - - [HTTP/HTTPs Reverse Proxy](#httphttps-reverse-proxy) - - [TCP Proxy](#tcp-proxy) - - [UDP Proxy](#udp-proxy) - - [Command-line args](#command-line-args) - - [Commands](#commands) - - [Use JSON Schema in VSCode](#use-json-schema-in-vscode) - - [Configuration](#configuration) - - [Labels (docker)](#labels-docker) - - [Environment variables](#environment-variables) - - [Config File](#config-file) - - [Fields](#fields) - - [Provider Kinds](#provider-kinds) - - [Provider File](#provider-file) - - [Supported DNS Challenge Providers](#supported-dns-challenge-providers) - - [Examples](#examples) - - [Single port configuration example](#single-port-configuration-example) - - [Multiple ports configuration example](#multiple-ports-configuration-example) - - [TCP/UDP configuration example](#tcpudp-configuration-example) - - [Load balancing Configuration Example](#load-balancing-configuration-example) - - [Troubleshooting](#troubleshooting) - - [Benchmarks](#benchmarks) - - [Known issues](#known-issues) - - [Memory usage](#memory-usage) - - [Build it yourself](#build-it-yourself) + +- [Table of content](#table-of-content) +- [Key Points](#key-points) +- [How to use](#how-to-use) +- [Tested Services](#tested-services) + - [HTTP/HTTPs Reverse Proxy](#httphttps-reverse-proxy) + - [TCP Proxy](#tcp-proxy) + - [UDP Proxy](#udp-proxy) +- [Command-line args](#command-line-args) + - [Commands](#commands) +- [Use JSON Schema in VSCode](#use-json-schema-in-vscode) +- [Environment variables](#environment-variables) +- [Config File](#config-file) + - [Fields](#fields) + - [Provider Kinds](#provider-kinds) + - [Provider File](#provider-file) + - [Supported DNS Challenge Providers](#supported-dns-challenge-providers) +- [Troubleshooting](#troubleshooting) +- [Benchmarks](#benchmarks) +- [Known issues](#known-issues) +- [Memory usage](#memory-usage) +- [Build it yourself](#build-it-yourself) + ## Key Points @@ -58,6 +52,8 @@ In the examples domain `x.y.z` is used, replace them with your domain ![config editor screenshot](screenshots/config_editor.png) +[🔼Back to top](#table-of-content) + ## How to use 1. Setup DNS Records to your machine's IP address @@ -65,18 +61,23 @@ In the examples domain `x.y.z` is used, replace them with your domain - A Record: `*.y.z` -> `10.0.10.1` - AAAA Record: `*.y.z` -> `::ffff:a00:a01` -2. Start `go-proxy` (see [Binary](docs/binary.md) or [docker](docs/docker.md)) +2. Start `go-proxy` by + + - [Running from binary or as a system service](docs/binary.md) + - [Running as a docker container](docs/docker.md) 3. Start editing config files - with text editor (i.e. Visual Studio Code) - - or with web config editor by navigate to `ip:8080` + - or with web config editor by navigate to `http://ip:8080` + +[🔼Back to top](#table-of-content) ## Tested Services ### HTTP/HTTPs Reverse Proxy -- nginx -- minio +- Nginx +- Minio - AdguardHome Dashboard - etc. @@ -91,6 +92,8 @@ In the examples domain `x.y.z` is used, replace them with your domain - Adguardhome DNS - Palworld Dedicated Server +[🔼Back to top](#table-of-content) + ## Command-line args `go-proxy [command]` @@ -106,9 +109,11 @@ Examples: - Binary: `go-proxy reload` - Docker: `docker exec -it go-proxy /app/go-proxy reload` +[🔼Back to top](#table-of-content) + ## Use JSON Schema in VSCode -Modify `.vscode/settings.json` to fit your needs +Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscode/settings.json` and modify to fit your needs ```json { @@ -125,78 +130,36 @@ Modify `.vscode/settings.json` to fit your needs } ``` -## Configuration +[🔼Back to top](#table-of-content) -With container name, no label needs to be added _(most of the time)_. - -### Labels (docker) - -See [docker.md](docs/docker.md#docker-compose-example) for examples - -When `go-proxy` is running in `host` network mode, see [here](docs/docker.md#docker-compose-example-host-network) for extra instructions - -- `proxy.aliases`: comma separated aliases for subdomain matching - - - default: container name - -- `proxy.*.`: wildcard label for all aliases - -Below labels has a **`proxy..`** prefix (i.e. `proxy.nginx.scheme: http`) - -- `scheme`: proxy protocol - - default: `http` - - allowed: `http`, `https`, `tcp`, `udp` -- `host`: proxy host - - default: `container_name` -- `port`: proxy port - - default: first expose port (declared in `Dockerfile` or `docker-compose.yml`) - - `http(s)`: number in range og `0 - 65535` - - `tcp/udp`: `[:]` - - `listeningPort`: number, when it is omitted (not suggested), a free port starting from 20000 will be used. - - `targetPort`: number, or predefined names (see [constants.go:14](src/go-proxy/constants.go#L14)) -- `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 - - - default: empty - - allowed: empty, `forward`, `sub` - - `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 - - `sub`: (experimental) remove path prefix from URL and also append path to HTML link attributes (`src`, `href` and `action`) and Javascript `fetch(url)` by response body substitution - e.g. apps.y.z/app1 -> webdav:80, `href="/app1/path/to/file"` -> `href="/path/to/file"` - -- `load_balance`: _(Docker only)_ enable load balance - - allowed: `1`, `true` - -### Environment variables +## Environment variables - `GOPROXY_DEBUG`: set to `1` or `true` to enable debug behaviors (i.e. output, etc.) - `GOPROXY_HOST_NETWORK`: _(Docker only)_ set to `1` when `network_mode: host` +- `GOPROXY_NO_SCHEMA_VALIDATION`: disable schema validation on config load / reload **(for testing new DNS Challenge providers)** -### Config File +[🔼Back to top](#table-of-content) + +## Config File See [config.example.yml](config.example.yml) for more -#### Fields +### Fields - `autocert`: autocert configuration - `email`: ACME Email - `domains`: a list of domains for cert registration - `provider`: DNS Challenge provider, see [Supported DNS Challenge Providers](#supported-dns-challenge-providers) - - `options`: provider specific options + - `options`: [provider specific options](#supported-dns-challenge-providers) - `providers`: reverse proxy providers configuration - `kind`: provider kind (string), see [Provider Kinds](#provider-kinds) - `value`: provider specific value -#### Provider Kinds +[🔼Back to top](#table-of-content) + +### Provider Kinds - `docker`: load reverse proxies from docker @@ -209,12 +172,16 @@ See [config.example.yml](config.example.yml) for more value: relative path of file to `config/` +[🔼Back to top](#table-of-content) + ### Provider File -Fields are same as [docker labels](#labels-docker) starting from `scheme` +Fields are same as [docker labels](docs/docker.md#labels) starting from `scheme` See [providers.example.yml](providers.example.yml) for examples +[🔼Back to top](#table-of-content) + ### Supported DNS Challenge Providers - Cloudflare @@ -223,85 +190,19 @@ See [providers.example.yml](providers.example.yml) for examples Follow [this guide](https://cloudkul.com/blog/automcatic-renew-and-generate-ssl-on-your-website-using-lego-client/) to create a new token with `Zone.DNS` read and edit permissions +- CloudDNS + + - `client_id` + - `email` + - `password` + +- DuckDNS (thanks [earvingad](https://github.com/earvingad)) + + - `token`: DuckDNS Token + To add more provider support, see [this](docs/add_dns_provider.md) -## Examples - -See [docker.md](docs/docker.md#docker-compose-example) for complete examples - -### Single port configuration example - -```yaml -# (default) https://.y.z -whoami: - image: traefik/whoami - container_name: whoami # => whoami.y.z - -# enable both subdomain and path matching: -whoami: - image: traefik/whoami - container_name: whoami - labels: - - proxy.aliases=whoami,apps - - proxy.apps.path=/whoami -# 1. visit https://whoami.y.z -# 2. visit https://apps.y.z/whoami -``` - -### Multiple ports configuration example - -```yaml -minio: - image: quay.io/minio/minio - container_name: minio - ... - labels: - - proxy.aliases=minio,minio-console - - proxy.minio.port=9000 - - proxy.minio-console.port=9001 - -# visit https://minio.y.z to access minio -# visit https://minio-console.y.z/whoami to access minio console -``` - -### TCP/UDP configuration example - -```yaml -# In the app -app-db: - image: postgres:15 - container_name: app-db - ... - labels: - # Optional (postgres is in the known image map) - - proxy.app-db.scheme=tcp - - # Optional (first free port will be used for listening port) - - proxy.app-db.port=20000:postgres - -# In go-proxy -go-proxy: - ... - ports: - - 80:80 - ... - - :20000/tcp - # or 20000-20010:20000-20010/tcp to declare large range at once - -# access app-db via <*>.y.z:20000 -``` - -## Load balancing Configuration Example - -```yaml -nginx: - ... - deploy: - mode: replicated - replicas: 3 - labels: - - proxy.nginx.load_balance=1 # allowed: [1, true] -``` +[🔼Back to top](#table-of-content) ## Troubleshooting @@ -309,6 +210,8 @@ Q: How to fix when it shows "no matching route for subdomain \"? A: Make sure the container is running, and \ matches any container name / alias +[🔼Back to top](#table-of-content) + ## Benchmarks Benchmarked with `wrk` connecting `traefik/whoami`'s `/bench` endpoint @@ -414,14 +317,20 @@ Local benchmark (client running wrk and `go-proxy` server are under same proxmox Transfer/sec: 10.94MB ``` +[🔼Back to top](#table-of-content) + ## Known issues -None +- Cert "renewal" is actually obtaining a new cert instead of renewing the existing one + +[🔼Back to top](#table-of-content) ## Memory usage It takes ~15 MB for 50 proxy entries +[🔼Back to top](#table-of-content) + ## Build it yourself 1. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already @@ -433,3 +342,5 @@ It takes ~15 MB for 50 proxy entries 4. build binary with `make build` 5. start your container with `make up` (docker) or `bin/go-proxy` (binary) + +[🔼Back to top](#table-of-content) diff --git a/docs/add_dns_provider.md b/docs/add_dns_provider.md index 16b07b2..1c04983 100644 --- a/docs/add_dns_provider.md +++ b/docs/add_dns_provider.md @@ -37,5 +37,5 @@ password: b9841238feb177a84330f ``` -5. Run and test if it works +5. Run with `GOPROXY_NO_SCHEMA_VALIDATION=1` and test if it works 6. Commit and create pull request diff --git a/docs/docker.md b/docs/docker.md index f1d8fba..f47fbc8 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,4 +1,24 @@ -# Getting started with `go-proxy` docker container +# Docker container guide + +## Table of content + + + +- [Table of content](#table-of-content) +- [Setup](#setup) +- [Labels](#labels) +- [Labels (docker specific)](#labels-docker-specific) +- [Troubleshooting](#troubleshooting) +- [Docker compose examples](#docker-compose-examples) + - [Local docker provider in bridge network](#local-docker-provider-in-bridge-network) + - [Remote docker provider](#remote-docker-provider) + - [Explaination](#explaination) + - [Remote setup](#remote-setup) + - [Proxy setup](#proxy-setup) + - [Local docker provider in host network](#local-docker-provider-in-host-network) + - [Proxy setup](#proxy-setup) + - [Services URLs for above examples](#services-urls-for-above-examples) + ## Setup @@ -46,6 +66,77 @@ 7. Start editing config files in `http://:8080` +[🔼Back to top](#table-of-content) + +## Labels + +- `proxy.aliases`: comma separated aliases for subdomain matching + + - default: container name + +- `proxy.*.`: wildcard label for all aliases + +Below labels has a **`proxy..`** prefix (i.e. `proxy.nginx.scheme: http`) + +- `scheme`: proxy protocol + - default: `http` + - allowed: `http`, `https`, `tcp`, `udp` +- `host`: proxy host + - default: `container_name` +- `port`: proxy port + - default: first expose port (declared in `Dockerfile` or `docker-compose.yml`) + - `http(s)`: number in range og `0 - 65535` + - `tcp/udp`: `[:]` + - `listeningPort`: number, when it is omitted (not suggested), a free port starting from 20000 will be used. + - `targetPort`: number, or predefined names (see [constants.go:14](src/go-proxy/constants.go#L14)) +- `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 + + - default: empty + - allowed: empty, `forward`, `sub` + + - `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 + - `sub`: **(experimental)** remove path prefix from URL and also append path to HTML link attributes (`src`, `href` and `action`) and Javascript `fetch(url)` by response body substitution + e.g. apps.y.z/app1 -> webdav:80, `href="/app1/path/to/file"` -> `href="/path/to/file"` + + - `set_headers`: a list of header to set, (key:value, one by line) + + Duplicated keys will be treated as multiple-value headers + + ```yaml + labels: + - | + proxy.app.set_headers= + X-Custom-Header1: value1 + X-Custom-Header1: value2 + X-Custom-Header2: value2 + ``` + + - `hide_headers`: comma seperated list of headers to hide + +[🔼Back to top](#table-of-content) + +## Labels (docker specific) + +Below labels has a **`proxy..`** prefix (i.e. `proxy.app.headers.hide: X-Powered-By,X-Custom-Header`) + +- `headers.set.
`: value of header to set + +- `headers.hide`: comma seperated list of headers to hide + +- `load_balance`: enable load balance + - allowed: `1`, `true` + +[🔼Back to top](#table-of-content) + ## Troubleshooting - Firewall issues @@ -64,7 +155,11 @@ `docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -` -## Docker compose example (bridge network) +[🔼Back to top](#table-of-content) + +## Docker compose examples + +### Local docker provider in bridge network ```yaml volumes: @@ -136,28 +231,19 @@ services: - /var/run/docker.sock:/var/run/docker.sock:ro labels: - proxy.aliases=gp - - proxy.panel.port=8080 + - proxy.gp.port=8080 ``` -### Services URLs +[🔼Back to top](#table-of-content) -- `gp.yourdomain.com`: go-proxy web panel -- `adg-setup.yourdomain.com`: adguard setup (first time setup) -- `adg.yourdomain.com`: adguard dashboard -- `nginx.yourdomain.com`: nginx -- `yourdomain.com:53`: adguard dns -- `yourdomain.com:25565`: minecraft server -- `yourdomain.com:8211`: palworld server +### Remote docker provider -## Docker compose example (host network) +#### Explaination -### Notice +- Expose container ports to random port in remote host +- Use container port with an asterisk sign **(\*)** before to find remote port automatically -When `go-proxy` is running in `host` network mode, you must: - -- set `GOPROXY_HOST_NETWORK=1` -- map ports to host explicitly -- add an asterisk sign **(*)** before `port` number under `labels` +#### Remote setup ```yaml volumes: @@ -170,7 +256,7 @@ services: adg: image: adguard/adguardhome restart: unless-stopped - ports: # map random ports to container ports + ports: # map container ports - 80 - 3000 - 53/udp @@ -224,19 +310,62 @@ services: - 80 volumes: - nginx:/usr/share/nginx/html - go-proxy: - image: ghcr.io/yusing/go-proxy - container_name: go-proxy - restart: always - network_mode: host # no port mapping needed for host network mode - environment: - - GOPROXY_HOST_NETWORK=1 # required for host network mode - volumes: - - ./config:/app/config - - /var/run/docker.sock:/var/run/docker.sock:ro - labels: - - proxy.aliases=gp - - proxy.panel.port=808 ``` -**Same services URLs as [`bridge`](#services-urls) example!** +[🔼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) + +### Local docker provider in host network + +Mostly as remote docker setup, see [remote setup](#remote-setup) + +With `GOPROXY_HOST_NETWORK=1` to treat it as remote docker provider + +#### Proxy setup + +```yaml +go-proxy: + image: ghcr.io/yusing/go-proxy + container_name: go-proxy + restart: always + network_mode: host + environment: # this part is needed for local docker in host mode + - GOPROXY_HOST_NETWORK=1 + 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 +- `adg-setup.yourdomain.com`: adguard setup (first time setup) +- `adg.yourdomain.com`: adguard dashboard +- `nginx.yourdomain.com`: nginx +- `yourdomain.com:53`: adguard dns +- `yourdomain.com:25565`: minecraft server +- `yourdomain.com:8211`: palworld server + +[🔼Back to top](#table-of-content) diff --git a/go.mod b/go.mod index 3c1c403..ddda21c 100755 --- a/go.mod +++ b/go.mod @@ -9,14 +9,14 @@ require ( github.com/go-acme/lego/v4 v4.16.1 github.com/santhosh-tekuri/jsonschema v1.2.4 github.com/sirupsen/logrus v1.9.3 - golang.org/x/net v0.22.0 + golang.org/x/net v0.24.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/Microsoft/go-winio v0.4.14 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cloudflare/cloudflare-go v0.86.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cloudflare/cloudflare-go v0.92.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -37,17 +37,18 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 // indirect + go.opentelemetry.io/otel v1.25.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.25.0 // indirect go.opentelemetry.io/otel/sdk v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/sys v0.18.0 // indirect + go.opentelemetry.io/otel/trace v1.25.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.17.0 // indirect + golang.org/x/tools v0.20.0 // indirect gotest.tools/v3 v3.5.1 // indirect ) diff --git a/go.sum b/go.sum index 7b9124a..6ab3afe 100755 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cloudflare/cloudflare-go v0.86.0 h1:jEKN5VHNYNYtfDL2lUFLTRo+nOVNPFxpXTstVx0rqHI= -github.com/cloudflare/cloudflare-go v0.86.0/go.mod h1:wYW/5UP02TUfBToa/yKbQHV+r6h1NnJ1Je7XjuGM4Jw= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cloudflare/cloudflare-go v0.92.0 h1:ltJvGvqZ4G6Fm2hHOYZ5RWpJQcrM0oDrsjjZydZhFJQ= +github.com/cloudflare/cloudflare-go v0.92.0/go.mod h1:nUqvBUUDRxNzsDSQjbqUNWHEIYAoUlgRmcAzMKlFdKs= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -58,7 +58,6 @@ github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -79,7 +78,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -88,61 +86,57 @@ github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XF github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 h1:cEPbyTSEHlQR89XVlyo78gqluF8Y3oMeBkXGWzQsfXY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8= +go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= +go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= +go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= +go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -153,8 +147,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/providers.example.yml b/providers.example.yml index dc5c026..b3103ee 100644 --- a/providers.example.yml +++ b/providers.example.yml @@ -1,16 +1,26 @@ example: # matching `app.y.z` # optional, defaults to http - scheme: + scheme: http # required, proxy target host: 10.0.0.1 # optional, defaults to 80 for http, 443 for https - port: 80 + port: "80" # optional, defaults to empty path: - # optional, defaults to sub + # optional, defaults to empty path_mode: # optional (https only) # no_tls_verify: false + # optional headers to set / override (http(s) only) + set_headers: + HEADER_A: + - VALUE_1 + - VALUE_2 + HEADER_B: [VALUE_3] + # optional headers to hide (http(s) only) + hide_headers: + - HEADER_C + - HEADER_D app1: # matching `app1.y.z` -> http://x.y.z host: x.y.z app2: # `app2` has no effect for tcp / udp, but still has to be unique across files @@ -22,4 +32,7 @@ app3: # matching `app3.y.z` -> https://10.0.0.1/app3 host: 10.0.0.1 path: /app3 path_mode: forward - no_tls_verify: false \ No newline at end of file + no_tls_verify: false + set_headers: + X-Forwarded-Proto: [https] + X-Forwarded-Host: [app3.y.z] diff --git a/schema/config.schema.json b/schema/config.schema.json index f3d7640..0bd9f5e 100644 --- a/schema/config.schema.json +++ b/schema/config.schema.json @@ -24,28 +24,89 @@ "provider": { "description": "DNS Challenge Provider", "type": "string", - "enum": ["cloudflare"] + "enum": ["cloudflare", "clouddns", "duckdns"] }, "options": { "description": "Provider specific options", - "type": "object", - "properties": { - "auth_token": { - "description": "Cloudflare API Token with Zone Scope", - "type": "string" - } - } + "type": "object" } }, "required": ["email", "domains", "provider", "options"], - "anyOf": [ + "allOf": [ { - "properties": { - "provider": { - "const": "cloudflare" - }, - "options": { - "required": ["auth_token"] + "if": { + "properties": { + "provider": { + "const": "cloudflare" + } + } + }, + "then": { + "properties": { + "options": { + "required": ["auth_token"], + "additionalProperties": false, + "properties": { + "auth_token": { + "description": "Cloudflare API Token with Zone Scope", + "type": "string" + } + } + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "clouddns" + } + } + }, + "then": { + "properties": { + "options": { + "required": ["client_id", "email", "password"], + "additionalProperties": false, + "properties": { + "client_id": { + "description": "CloudDNS Client ID", + "type": "string" + }, + "email": { + "description": "CloudDNS Email", + "type": "string" + }, + "password": { + "description": "CloudDNS Password", + "type": "string" + } + } + } + } + } + }, + { + "if": { + "properties": { + "provider": { + "const": "duckdns" + } + } + }, + "then": { + "properties": { + "options": { + "required": ["token"], + "additionalProperties": false, + "properties": { + "token": { + "description": "DuckDNS Token", + "type": "string" + } + } + } } } } diff --git a/schema/providers.schema.json b/schema/providers.schema.json index 373ffec..c689910 100644 --- a/schema/providers.schema.json +++ b/schema/providers.schema.json @@ -55,7 +55,9 @@ "no_tls_verify": { "description": "Disable TLS verification for https proxy", "type": "boolean" - } + }, + "set_headers": {}, + "hide_headers": {} }, "required": ["host"], "additionalProperties": false, @@ -129,6 +131,23 @@ "type": "null" } ] + }, + "set_headers": { + "type": "object", + "description": "Proxy headers to set", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "hide_headers": { + "type":"array", + "description": "Proxy headers to hide", + "items": { + "type": "string" + } } } }, @@ -145,6 +164,12 @@ }, "path_mode": { "not": true + }, + "set_headers": { + "not": true + }, + "hide_headers": { + "not": true } }, "required": ["port"] diff --git a/setup-binary.sh b/setup-binary.sh index d01a146..6122900 100644 --- a/setup-binary.sh +++ b/setup-binary.sh @@ -98,7 +98,7 @@ Wants=network-online.target systemd-networkd-wait-online.service Type=simple ExecStart=${APP_ROOT}/bin/go-proxy WorkingDirectory=${APP_ROOT} -Environment="IS_SYSTEMD=1" +Environment="GOPROXY_IS_SYSTEMD=1" Restart=on-failure RestartSec=1s KillMode=process diff --git a/src/go-proxy/autocert.go b/src/go-proxy/autocert.go index 0ac17b7..fa57be5 100644 --- a/src/go-proxy/autocert.go +++ b/src/go-proxy/autocert.go @@ -9,6 +9,7 @@ import ( "crypto/x509" "os" "path" + "slices" "sync" "time" @@ -18,6 +19,7 @@ import ( "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/providers/dns/clouddns" "github.com/go-acme/lego/v4/providers/dns/cloudflare" + "github.com/go-acme/lego/v4/providers/dns/duckdns" "github.com/go-acme/lego/v4/registration" ) @@ -54,7 +56,7 @@ type AutoCertProvider interface { GetExpiries() CertExpiries LoadCert() bool ObtainCert() NestedErrorLike - RenewalOn() time.Time + ShouldRenewOn() time.Time ScheduleRenewal() } @@ -72,7 +74,7 @@ func (cfg AutoCertConfig) GetProvider() (AutoCertProvider, error) { } gen, ok := providersGenMap[cfg.Provider] if !ok { - ne.Extraf("unknown provider: %s", cfg.Provider) + ne.Extraf("unknown provider: %q", cfg.Provider) } if ne.HasExtras() { return nil, ne @@ -189,13 +191,9 @@ func (p *autoCertProvider) LoadCert() bool { return true } -func (p *autoCertProvider) RenewalOn() time.Time { - t := time.Now().AddDate(0, 0, 3) +func (p *autoCertProvider) ShouldRenewOn() time.Time { for _, expiry := range p.certExpiries { - if expiry.Before(t) { - return time.Now() - } - return t + return expiry.AddDate(0, -1, 0) } // this line should never be reached panic("no certificate available") @@ -203,8 +201,8 @@ func (p *autoCertProvider) RenewalOn() time.Time { func (p *autoCertProvider) ScheduleRenewal() { for { - t := time.Until(p.RenewalOn()) - aclog.Infof("next renewal in %v", t) + t := time.Until(p.ShouldRenewOn()) + aclog.Infof("next renewal in %v", t.Round(time.Second)) time.Sleep(t) err := p.renewIfNeeded() if err != nil { @@ -230,7 +228,29 @@ func (p *autoCertProvider) saveCert(cert *certificate.Resource) NestedErrorLike } func (p *autoCertProvider) needRenewal() bool { - return time.Now().After(p.RenewalOn()) + expired := time.Now().After(p.ShouldRenewOn()) + if expired { + return true + } + if len(p.cfg.Domains) != len(p.certExpiries) { + return true + } + wantedDomains := make([]string, len(p.cfg.Domains)) + certDomains := make([]string, len(p.certExpiries)) + copy(wantedDomains, p.cfg.Domains) + i := 0 + for domain := range p.certExpiries { + certDomains[i] = domain + i++ + } + slices.Sort(wantedDomains) + slices.Sort(certDomains) + for i, domain := range certDomains { + if domain != wantedDomains[i] { + return true + } + } + return false } func (p *autoCertProvider) renewIfNeeded() NestedErrorLike { @@ -249,6 +269,7 @@ func (p *autoCertProvider) renewIfNeeded() NestedErrorLike { for { err := p.ObtainCert() if err == nil { + aclog.Info("renewed certificate") return nil } trials++ @@ -305,5 +326,6 @@ func setOptions[T interface{}](cfg *T, opt ProviderOptions) error { var providersGenMap = map[string]ProviderGenerator{ "cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig), - "clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig), + "clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig), + "duckdns": providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig), } diff --git a/src/go-proxy/config.go b/src/go-proxy/config.go index d220f14..21af8a6 100644 --- a/src/go-proxy/config.go +++ b/src/go-proxy/config.go @@ -1,10 +1,10 @@ package main import ( - "os" "sync" "time" + "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) @@ -25,12 +25,10 @@ type Config interface { func NewConfig(path string) Config { cfg := &config{ reader: &FileReader{Path: path}, + l: cfgl, } - cfg.watcher = NewFileWatcher( - path, - cfg.MustReload, // OnChange - func() { os.Exit(1) }, // OnDelete - ) + // must init fields above before creating watcher + cfg.watcher = cfg.NewFileWatcher() return cfg } @@ -43,10 +41,7 @@ func (cfg *config) Value() configModel { return *cfg.m } -func (cfg *config) Load(reader ...Reader) error { - cfg.mutex.Lock() - defer cfg.mutex.Unlock() - +func (cfg *config) Load() error { if cfg.reader == nil { panic("config reader not set") } @@ -68,7 +63,7 @@ func (cfg *config) Load(reader ...Reader) error { ne.With(err) } - pErrs := NewNestedError("errors in these providers") + pErrs := NewNestedError("these providers have errors") for name, p := range model.Providers { if p.Kind != ProviderKind_File { @@ -90,13 +85,16 @@ func (cfg *config) Load(reader ...Reader) error { return ne } + cfg.mutex.Lock() + defer cfg.mutex.Unlock() + cfg.m = model return nil } func (cfg *config) MustLoad() { if err := cfg.Load(); err != nil { - cfgl.Fatal(err) + cfg.l.Fatal(err) } } @@ -115,7 +113,7 @@ func (cfg *config) Reload() error { func (cfg *config) MustReload() { if err := cfg.Reload(); err != nil { - cfgl.Fatal(err) + cfg.l.Fatal(err) } } @@ -144,7 +142,7 @@ func (cfg *config) StartProviders() { cfg.providerInitialized = true if pErrs.HasExtras() { - cfgl.Error(pErrs) + cfg.l.Error(pErrs) } } @@ -194,6 +192,7 @@ func defaultConfig() *configModel { type config struct { m *configModel + l logrus.FieldLogger reader Reader watcher Watcher mutex sync.Mutex diff --git a/src/go-proxy/constants.go b/src/go-proxy/constants.go index 39192a8..5e42a7f 100644 --- a/src/go-proxy/constants.go +++ b/src/go-proxy/constants.go @@ -123,7 +123,7 @@ var ( } ) -const wildcardLabelPrefix = "proxy.*." +const wildcardAlias = "*" const clientUrlFromEnv = "FROM_ENV" @@ -147,18 +147,6 @@ const ( var ( configSchema *jsonschema.Schema providersSchema *jsonschema.Schema - _ = func() *jsonschema.Compiler { - c := jsonschema.NewCompiler() - c.Draft = jsonschema.Draft7 - var err error - if configSchema, err = c.Compile(configSchemaPath); err != nil { - panic(err) - } - if providersSchema, err = c.Compile(providersSchemaPath); err != nil { - panic(err) - } - return c - }() ) const ( @@ -168,17 +156,36 @@ const ( const udpBufferSize = 1500 -var isHostNetworkMode = os.Getenv("GOPROXY_HOST_NETWORK") == "1" +var isHostNetworkMode = getEnvBool("GOPROXY_HOST_NETWORK") var logLevel = func() logrus.Level { - switch os.Getenv("GOPROXY_DEBUG") { - case "1", "true": + if getEnvBool("GOPROXY_DEBUG") { logrus.SetLevel(logrus.DebugLevel) } return logrus.GetLevel() }() -var isRunningAsService = func() bool { - v := os.Getenv("IS_SYSTEMD") - return v == "1" -}() \ No newline at end of file +var isRunningAsService = getEnvBool("IS_SYSTEMD") || getEnvBool("GOPROXY_IS_SYSTEMD") // IS_SYSTEMD is deprecated + +var noSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION") + +func getEnvBool(key string) bool { + v := os.Getenv(key) + return v == "1" || v == "true" +} + +func initSchema() { + if noSchemaValidation { + return + } + + c := jsonschema.NewCompiler() + c.Draft = jsonschema.Draft7 + var err error + if configSchema, err = c.Compile(configSchemaPath); err != nil { + panic(err) + } + if providersSchema, err = c.Compile(providersSchemaPath); err != nil { + panic(err) + } +} \ No newline at end of file diff --git a/src/go-proxy/docker_provider.go b/src/go-proxy/docker_provider.go index ccdb609..3ac66c7 100755 --- a/src/go-proxy/docker_provider.go +++ b/src/go-proxy/docker_provider.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "net/http" "strconv" @@ -14,20 +15,15 @@ import ( "golang.org/x/net/context" ) -func (p *Provider) setConfigField(c *ProxyConfig, label string, value string, prefix string) error { - if strings.HasPrefix(label, prefix) { - field := strings.TrimPrefix(label, prefix) - if err := setFieldFromSnake(c, field, value); err != nil { - return err - } - } - return nil +func setConfigField(pl *ProxyLabel, c *ProxyConfig) error { + return setFieldFromSnake(c, pl.Field, pl.Value) } -func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP string) ProxyConfigSlice { +func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP string) (ProxyConfigSlice, error) { var aliases []string cfgs := make(ProxyConfigSlice, 0) + cfgMap := make(map[string]*ProxyConfig) containerName := strings.TrimPrefix(container.Names[0], "/") aliasesLabel, ok := container.Labels["proxy.aliases"] @@ -35,7 +31,8 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP if !ok { aliases = []string{containerName} } else { - aliases = strings.Split(aliasesLabel, ",") + v, _ := commaSepParser(aliasesLabel) + aliases = v.([]string) } if clientIP == "" && isHostNetworkMode { @@ -44,21 +41,42 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP isRemote := clientIP != "" for _, alias := range aliases { - ne := NewNestedError("invalid label config").Subjectf("container %s", containerName) + cfgMap[alias] = &ProxyConfig{} + } - l := p.l.WithField("container", containerName).WithField("alias", alias) - config := NewProxyConfig(p) - prefix := fmt.Sprintf("proxy.%s.", alias) - for label, value := range container.Labels { - err := p.setConfigField(&config, label, value, prefix) - if err != nil { - ne.ExtraError(NewNestedErrorFrom(err).Subjectf("alias %s", alias)) - } - err = p.setConfigField(&config, label, value, wildcardLabelPrefix) - if err != nil { - ne.ExtraError(NewNestedErrorFrom(err).Subjectf("alias %s", alias)) + ne := NewNestedError("these labels have errors").Subject(containerName) + + for label, value := range container.Labels { + pl, err := parseProxyLabel(label, value) + if err != nil { + if !errors.Is(err, errNotProxyLabel) { + ne.ExtraError(NewNestedErrorFrom(err).Subject(label)) } + continue } + if pl.Alias == wildcardAlias { + for alias := range cfgMap { + pl.Alias = alias + err = setConfigField(pl, cfgMap[alias]) + if err != nil { + ne.ExtraError(NewNestedErrorFrom(err).Subject(pl.Alias)) + } + } + continue + } + config, ok := cfgMap[pl.Alias] + if !ok { + ne.ExtraError(NewNestedError("unknown alias").Subject(pl.Alias)) + continue + } + err = setConfigField(pl, config) + if err != nil { + ne.ExtraError(NewNestedErrorFrom(err).Subject(pl.Alias)) + } + } + + for alias, config := range cfgMap { + l := p.l.WithField("alias", alias) if config.Port == "" { config.Port = fmt.Sprintf("%d", selectPort(container, isRemote)) } @@ -70,8 +88,6 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP switch { case strings.HasSuffix(config.Port, "443"): config.Scheme = "https" - case strings.HasPrefix(container.Image, "sha256:"): - config.Scheme = "http" default: imageName := getImageName(container) _, isKnownImage := ImageNamePortMapTCP[imageName] @@ -90,7 +106,7 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP var err error // find matching port srcPort := config.Port[1:] - config.Port, err = findMatchingContainerPort(container,srcPort) + config.Port, err = findMatchingContainerPort(container, srcPort) if err != nil { ne.ExtraError(NewNestedErrorFrom(err).Subjectf("alias %s", alias)) } @@ -98,8 +114,7 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP config.Port = fmt.Sprintf("%s:%s", srcPort, config.Port) } } - - + if config.Host == "" { switch { case isRemote: @@ -126,12 +141,15 @@ func (p *Provider) getContainerProxyConfigs(container *types.Container, clientIP config.Alias = alias if ne.HasExtras() { - l.Error(ne) continue } - cfgs = append(cfgs, config) + cfgs = append(cfgs, *config) } - return cfgs + + if ne.HasExtras() { + return nil, ne + } + return cfgs, nil } func (p *Provider) getDockerClient() (*client.Client, error) { @@ -196,8 +214,19 @@ func (p *Provider) getDockerProxyConfigs() (ProxyConfigSlice, error) { cfgs := make(ProxyConfigSlice, 0) + ne := NewNestedError("these containers have errors") for _, container := range containerSlice { - cfgs = append(cfgs, p.getContainerProxyConfigs(&container, clientIP)...) + ccfgs, err := p.getContainerProxyConfigs(&container, clientIP) + if err != nil { + ne.ExtraError(err) + continue + } + cfgs = append(cfgs, ccfgs...) + } + + if ne.HasExtras() { + // print but ignore + p.l.Error(ne) } return cfgs, nil diff --git a/src/go-proxy/error.go b/src/go-proxy/error.go index d575e9c..7b5b448 100644 --- a/src/go-proxy/error.go +++ b/src/go-proxy/error.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "strings" "sync" @@ -46,6 +47,10 @@ func NewNestedErrorFrom(err error) NestedErrorLike { if err == nil { panic("cannot convert nil error to NestedError") } + errUnwrap := errors.Unwrap(err) + if errUnwrap != nil { + return NewNestedErrorFrom(errUnwrap) + } return NewNestedError(err.Error()) } @@ -92,23 +97,23 @@ func (ne *NestedError) Level() int { return ne.level } -func (ef *NestedError) Error() string { +func (ne *NestedError) Error() string { var buf strings.Builder - ef.writeToSB(&buf, "") + ne.writeToSB(&buf, ne.level, "") return buf.String() } -func (ef *NestedError) HasInner() bool { - return ef.inner != nil +func (ne *NestedError) HasInner() bool { + return ne.inner != nil } -func (ef *NestedError) HasExtras() bool { - return len(ef.extras) > 0 +func (ne *NestedError) HasExtras() bool { + return len(ne.extras) > 0 } -func (ef *NestedError) With(inner error) NestedErrorLike { - ef.Lock() - defer ef.Unlock() +func (ne *NestedError) With(inner error) NestedErrorLike { + ne.Lock() + defer ne.Unlock() var in *NestedError @@ -116,79 +121,75 @@ func (ef *NestedError) With(inner error) NestedErrorLike { case NestedErrorLike: in = t.copy() default: - in = &NestedError{extras: []string{t.Error()}} + in = &NestedError{message: t.Error()} } - if ef.inner == nil { - ef.inner = in + if ne.inner == nil { + ne.inner = in } else { - ef.inner.ExtraError(in) + ne.inner.ExtraError(in) } - root := ef + root := ne for root.inner != nil { root.inner.level = root.level + 1 root = root.inner } - return ef + return ne } -func (ef *NestedError) addLevel(level int) NestedErrorLike { - ef.level += level - if ef.inner != nil { - ef.inner.addLevel(level) +func (ne *NestedError) addLevel(level int) NestedErrorLike { + ne.level += level + if ne.inner != nil { + ne.inner.addLevel(level) } - return ef + return ne } -func (ef *NestedError) copy() *NestedError { +func (ne *NestedError) copy() *NestedError { var inner *NestedError - if ef.inner != nil { - inner = ef.inner.copy() + if ne.inner != nil { + inner = ne.inner.copy() } return &NestedError{ - subject: ef.subject, - message: ef.message, - extras: ef.extras, + subject: ne.subject, + message: ne.message, + extras: ne.extras, inner: inner, - level: ef.level, } } -func (ef *NestedError) writeIndents(sb *strings.Builder, level int) { +func (ne *NestedError) writeIndents(sb *strings.Builder, level int) { for i := 0; i < level; i++ { sb.WriteString(" ") } } -func (ef *NestedError) writeToSB(sb *strings.Builder, prefix string) { - ef.writeIndents(sb, ef.level) +func (ne *NestedError) writeToSB(sb *strings.Builder, level int, prefix string) { + ne.writeIndents(sb, level) sb.WriteString(prefix) - if ef.subject != "" { - sb.WriteRune('"') - sb.WriteString(ef.subject) - sb.WriteRune('"') - if ef.message != "" { - sb.WriteString(":\n") - } else { - sb.WriteRune('\n') + if ne.subject != "" { + sb.WriteString(ne.subject) + if ne.message != "" { + sb.WriteString(": ") } } - if ef.message != "" { - ef.writeIndents(sb, ef.level) - sb.WriteString(ef.message) - sb.WriteRune('\n') + if ne.message != "" { + sb.WriteString(ne.message) } - for _, l := range ef.extras { - l = strings.TrimSpace(l) + if ne.HasExtras() || ne.HasInner() { + sb.WriteString(":\n") + } + level += 1 + for _, l := range ne.extras { if l == "" { continue } - ef.writeIndents(sb, ef.level) + ne.writeIndents(sb, level) sb.WriteString("- ") sb.WriteString(l) sb.WriteRune('\n') } - if ef.inner != nil { - ef.inner.writeToSB(sb, "- ") + if ne.inner != nil { + ne.inner.writeToSB(sb, level, "- ") } } diff --git a/src/go-proxy/error_test.go b/src/go-proxy/error_test.go new file mode 100644 index 0000000..7ac5ddf --- /dev/null +++ b/src/go-proxy/error_test.go @@ -0,0 +1,66 @@ +package main + +import ( + "testing" +) + +func AssertEq(t *testing.T, got, want string) { + t.Helper() + if got != want { + t.Errorf("expected %q, got %q", want, got) + } +} + +func TestErrorSimple(t *testing.T) { + ne := NewNestedError("foo bar") + AssertEq(t, ne.Error(), "foo bar") + ne.Subject("baz") + AssertEq(t, ne.Error(), "baz: foo bar") +} + +func TestErrorSubjectOnly(t *testing.T) { + ne := NewNestedError("").Subject("bar") + AssertEq(t, ne.Error(), "bar") +} + +func TestErrorExtra(t *testing.T) { + ne := NewNestedError("foo").Extra("bar").Extra("baz") + AssertEq(t, ne.Error(), "foo:\n - bar\n - baz\n") +} + +func TestErrorNested(t *testing.T) { + inner := NewNestedError("inner"). + Extra("123"). + Extra("456") + inner2 := NewNestedError("inner"). + Subject("2"). + Extra("456"). + Extra("789") + inner3 := NewNestedError("inner"). + Subject("3"). + Extra("456"). + Extra("789") + ne := NewNestedError("foo"). + Extra("bar"). + Extra("baz"). + ExtraError(inner). + With(inner.With(inner2.With(inner3))) + want := + `foo: + - bar + - baz + - inner: + - 123 + - 456 + - inner: + - 123 + - 456 + - 2: inner: + - 456 + - 789 + - 3: inner: + - 456 + - 789 +` + AssertEq(t, ne.Error(), want) +} diff --git a/src/go-proxy/http_route.go b/src/go-proxy/http_route.go index bb2e866..afce379 100755 --- a/src/go-proxy/http_route.go +++ b/src/go-proxy/http_route.go @@ -34,7 +34,7 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) { tr = transport } - proxy := NewSingleHostReverseProxy(url, tr) + proxy := NewReverseProxy(url, tr, config) route := &HTTPRoute{ Alias: config.Alias, @@ -42,36 +42,35 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) { Path: config.Path, Proxy: proxy, PathMode: config.PathMode, - l: hrlog.WithFields(logrus.Fields{ - "alias": config.Alias, - // "path": config.Path, - // "path_mode": config.PathMode, - }), + l: logrus.WithField("alias", config.Alias), } var rewriteBegin = proxy.Rewrite var rewrite func(*ProxyRequest) var modifyResponse func(*http.Response) error - switch { - case config.Path == "", config.PathMode == ProxyPathMode_Forward: + // no path or forward path + if config.Path == "" || config.PathMode == ProxyPathMode_Forward { rewrite = rewriteBegin - case config.PathMode == ProxyPathMode_RemovedPath: - rewrite = func(pr *ProxyRequest) { - rewriteBegin(pr) - pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path) + } else { + switch config.PathMode { + case ProxyPathMode_RemovedPath: + rewrite = func(pr *ProxyRequest) { + rewriteBegin(pr) + pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path) + } + case ProxyPathMode_Sub: + rewrite = func(pr *ProxyRequest) { + rewriteBegin(pr) + // disable compression + pr.Out.Header.Set("Accept-Encoding", "identity") + // remove path prefix + pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path) + } + modifyResponse = config.pathSubModResp + default: + return nil, NewNestedError("invalid path mode").Subject(config.PathMode) } - case config.PathMode == ProxyPathMode_Sub: - rewrite = func(pr *ProxyRequest) { - rewriteBegin(pr) - // disable compression - pr.Out.Header.Set("Accept-Encoding", "identity") - // remove path prefix - pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path) - } - modifyResponse = config.pathSubModResp - default: - return nil, NewNestedError("invalid path mode").Subject(config.PathMode) } if logLevel == logrus.DebugLevel { @@ -96,8 +95,9 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) { } func (r *HTTPRoute) Start() { - // dummy + httpRoutes.Get(r.Alias).Add(r.Path, r) } + func (r *HTTPRoute) Stop() { httpRoutes.Delete(r.Alias) } diff --git a/src/go-proxy/loggers.go b/src/go-proxy/loggers.go index 9f0b3fa..2edd6ad 100644 --- a/src/go-proxy/loggers.go +++ b/src/go-proxy/loggers.go @@ -2,10 +2,9 @@ package main import "github.com/sirupsen/logrus" -var palog = logrus.WithField("component", "panel") -var prlog = logrus.WithField("component", "provider") -var cfgl = logrus.WithField("component", "config") -var hrlog = logrus.WithField("component", "http_proxy") -var srlog = logrus.WithField("component", "stream") -var wlog = logrus.WithField("component", "watcher") -var aclog = logrus.WithField("component", "autocert") \ No newline at end of file +var palog = logrus.WithField("?", "panel") +var cfgl = logrus.WithField("?", "config") +var hrlog = logrus.WithField("?", "http") +var srlog = logrus.WithField("?", "stream") +var wlog = logrus.WithField("?", "watcher") +var aclog = logrus.WithField("?", "autocert") \ No newline at end of file diff --git a/src/go-proxy/main.go b/src/go-proxy/main.go index d4eb58d..f8eb494 100755 --- a/src/go-proxy/main.go +++ b/src/go-proxy/main.go @@ -43,6 +43,8 @@ func main() { return } + initSchema() + cfg = NewConfig(configPath) cfg.MustLoad() diff --git a/src/go-proxy/provider.go b/src/go-proxy/provider.go index d6209d6..c7a2aad 100644 --- a/src/go-proxy/provider.go +++ b/src/go-proxy/provider.go @@ -1,8 +1,6 @@ package main import ( - "sync" - "github.com/sirupsen/logrus" ) @@ -10,15 +8,17 @@ type Provider struct { Kind string `json:"kind"` // docker, file Value string `json:"value"` - watcher Watcher - routes map[string]Route // id -> Route - mutex sync.Mutex - l logrus.FieldLogger + watcher Watcher + routes map[string]Route // id -> Route + l logrus.FieldLogger + reloadReqCh chan struct{} } // Init is called after LoadProxyConfig func (p *Provider) Init(name string) error { - p.l = prlog.WithFields(logrus.Fields{"kind": p.Kind, "name": name}) + p.l = logrus.WithField("provider", name) + p.reloadReqCh = make(chan struct{}, 1) + defer p.initWatcher() if err := p.loadProxyConfig(); err != nil { @@ -40,16 +40,23 @@ func (p *Provider) StopAllRoutes() { } func (p *Provider) ReloadRoutes() { - p.mutex.Lock() - defer p.mutex.Unlock() + select { + case p.reloadReqCh <- struct{}{}: + defer func() { + <-p.reloadReqCh + }() - p.StopAllRoutes() - err := p.loadProxyConfig() - if err != nil { - p.l.Error("failed to reload routes: ", err) + p.StopAllRoutes() + err := p.loadProxyConfig() + if err != nil { + p.l.Error("failed to reload routes: ", err) + return + } + p.StartAllRoutes() + default: + p.l.Info("reload request already in progress") return } - p.StartAllRoutes() } func (p *Provider) loadProxyConfig() error { @@ -97,9 +104,9 @@ func (p *Provider) initWatcher() error { if err != nil { return NewNestedError("unable to create docker client").With(err) } - p.watcher = NewDockerWatcher(dockerClient, p.ReloadRoutes) + p.watcher = p.NewDockerWatcher(dockerClient) case ProviderKind_File: - p.watcher = NewFileWatcher(p.GetFilePath(), p.ReloadRoutes, p.StopAllRoutes) + p.watcher = p.NewFileWatcher() } return nil } diff --git a/src/go-proxy/proxy_config.go b/src/go-proxy/proxy_config.go index c735ca6..4d033e0 100644 --- a/src/go-proxy/proxy_config.go +++ b/src/go-proxy/proxy_config.go @@ -1,29 +1,26 @@ package main -import "fmt" +import ( + "fmt" + "net/http" +) type ProxyConfig struct { - Alias string `yaml:"-" json:"-"` - Scheme string `yaml:"scheme" json:"scheme"` - Host string `yaml:"host" json:"host"` - Port string `yaml:"port" json:"port"` - LoadBalance string `yaml:"-" json:"-"` // docker provider only - NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // http proxy only - Path string `yaml:"path" json:"path"` // http proxy only - PathMode string `yaml:"path_mode" json:"path_mode"` // http proxy only - - provider *Provider + Alias string `yaml:"-" json:"-"` + Scheme string `yaml:"scheme" json:"scheme"` + Host string `yaml:"host" json:"host"` + Port string `yaml:"port" json:"port"` + LoadBalance string `yaml:"-" json:"-"` // docker provider only + NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // http proxy only + Path string `yaml:"path" json:"path"` // http proxy only + PathMode string `yaml:"path_mode" json:"path_mode"` // http proxy only + SetHeaders http.Header `yaml:"set_headers" json:"set_headers"` // http proxy only + HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http proxy only } type ProxyConfigMap map[string]ProxyConfig type ProxyConfigSlice []ProxyConfig -func NewProxyConfig(provider *Provider) ProxyConfig { - return ProxyConfig{ - provider: provider, - } -} - // used by `GetFileProxyConfigs` func (cfg *ProxyConfig) SetDefaults() error { err := NewNestedError("invalid proxy config").Subject(cfg.Alias) @@ -55,4 +52,4 @@ func (cfg *ProxyConfig) SetDefaults() error { func (cfg *ProxyConfig) GetID() string { return fmt.Sprintf("%s-%s-%s-%s-%s", cfg.Alias, cfg.Scheme, cfg.Host, cfg.Port, cfg.Path) -} +} \ No newline at end of file diff --git a/src/go-proxy/proxy_label.go b/src/go-proxy/proxy_label.go new file mode 100644 index 0000000..20997c7 --- /dev/null +++ b/src/go-proxy/proxy_label.go @@ -0,0 +1,92 @@ +package main + +import ( + "errors" + "fmt" + "net/http" + "strings" +) + +type ProxyLabel struct { + Alias string + Field string + Value any +} + +var errNotProxyLabel = errors.New("not a proxy label") +var errInvalidSetHeaderLine = errors.New("invalid set header line") +var errInvalidBoolean = errors.New("invalid boolean") + +const proxyLabelNamespace = "proxy" + +func parseProxyLabel(label string, value string) (*ProxyLabel, error) { + ns := strings.Split(label, ".") + var v any = value + + if len(ns) != 3 { + return nil, errNotProxyLabel + } + + if ns[0] != proxyLabelNamespace { + return nil, errNotProxyLabel + } + + field := ns[2] + + var err error + parser, ok := valueParser[field] + + if ok { + v, err = parser(v.(string)) + if err != nil { + return nil, err + } + } + + return &ProxyLabel{ + Alias: ns[1], + Field: field, + Value: v, + }, nil +} + +func setHeadersParser(value string) (any, error) { + value = strings.TrimSpace(value) + lines := strings.Split(value, "\n") + h := make(http.Header) + for _, line := range lines { + parts := strings.SplitN(line, ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("%w: %q", errInvalidSetHeaderLine, line) + } + key := strings.TrimSpace(parts[0]) + val := strings.TrimSpace(parts[1]) + h.Add(key, val) + } + return h, nil +} + +func commaSepParser(value string) (any, error) { + v := strings.Split(value, ",") + for i := range v { + v[i] = strings.TrimSpace(v[i]) + } + return v, nil +} + +func boolParser(value string) (any, error) { + switch strings.ToLower(value) { + case "true", "yes", "1": + return true, nil + case "false", "no", "0": + return false, nil + default: + return nil, fmt.Errorf("%w: %q", errInvalidBoolean, value) + } +} + +var valueParser = map[string]func(string) (any, error){ + "set_headers": setHeadersParser, + "hide_headers": commaSepParser, + "no_tls_verify": boolParser, +} diff --git a/src/go-proxy/proxy_label_test.go b/src/go-proxy/proxy_label_test.go new file mode 100644 index 0000000..ab712c1 --- /dev/null +++ b/src/go-proxy/proxy_label_test.go @@ -0,0 +1,186 @@ +package main + +import ( + "errors" + "fmt" + "net/http" + "reflect" + "testing" +) + +func makeLabel(alias string, field string) string { + return fmt.Sprintf("proxy.%s.%s", alias, field) +} + +func TestNotProxyLabel(t *testing.T) { + pl, err := parseProxyLabel("foo.bar", "1234") + if !errors.Is(err, errNotProxyLabel) { + t.Errorf("expected err NotProxyLabel, got %v", err) + } + if pl != nil { + t.Errorf("expected nil, got %v", pl) + } + _, err = parseProxyLabel("proxy.foo", "bar") + if !errors.Is(err, errNotProxyLabel) { + t.Errorf("expected err InvalidProxyLabel, got %v", err) + } +} + +func TestStringProxyLabel(t *testing.T) { + alias := "foo" + field := "ip" + v := "bar" + pl, err := parseProxyLabel(makeLabel(alias, field), v) + if err != nil { + t.Errorf("expected err=nil, got %v", err) + } + if pl.Alias != alias { + t.Errorf("expected alias=%s, got %s", alias, pl.Alias) + } + if pl.Field != field { + t.Errorf("expected field=%s, got %s", field, pl.Field) + } + 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, + "yes": true, + "1": true, + "false": false, + "FALSE": false, + "no": false, + "0": false, + } + + for k, v := range tests { + pl, err := parseProxyLabel(makeLabel(alias, field), k) + if err != nil { + t.Errorf("expected err=nil, got %v", err) + } + if pl.Alias != alias { + t.Errorf("expected alias=%s, got %s", alias, pl.Alias) + } + if pl.Field != field { + t.Errorf("expected field=%s, got %s", field, pl.Field) + } + if pl.Value != v { + t.Errorf("expected value=%v, got %v", v, pl.Value) + } + } +} + +func TestBoolProxyLabelInvalid(t *testing.T) { + alias := "foo" + field := "no_tls_verify" + _, err := parseProxyLabel(makeLabel(alias, field), "invalid") + if !errors.Is(err, errInvalidBoolean) { + t.Errorf("expected err InvalidProxyLabel, got %v", err) + } +} + +func TestHeaderProxyLabelValid(t *testing.T) { + alias := "foo" + field := "set_headers" + v := ` + X-Custom-Header1: foo + X-Custom-Header1: bar + X-Custom-Header2: baz + ` + h := make(http.Header, 0) + h.Set("X-Custom-Header1", "foo") + h.Add("X-Custom-Header1", "bar") + h.Set("X-Custom-Header2", "baz") + + pl, err := parseProxyLabel(makeLabel(alias, field), v) + if err != nil { + t.Errorf("expected err=nil, got %v", err) + } + if pl.Alias != alias { + t.Errorf("expected alias=%s, got %s", alias, pl.Alias) + } + if pl.Field != field { + t.Errorf("expected field=%s, got %s", field, pl.Field) + } + hGot, ok := pl.Value.(http.Header) + if !ok { + t.Error("value is not http.Header") + return + } + for k, vWant := range h { + vGot := hGot[k] + if !reflect.DeepEqual(vGot, vWant) { + t.Errorf("expected %s=%q, got %q", k, vWant, vGot) + } + } +} + +func TestHeaderProxyLabelInvalid(t *testing.T) { + alias := "foo" + field := "set_headers" + tests := []string{ + "X-Custom-Header1 = bar", + "X-Custom-Header1", + } + + for _, v := range tests { + _, err := parseProxyLabel(makeLabel(alias, field), v) + if !errors.Is(err, errInvalidSetHeaderLine) { + t.Errorf("expected err InvalidProxyLabel for %q, got %v", v, err) + } + } +} + +func TestCommaSepProxyLabelSingle(t *testing.T) { + alias := "foo" + field := "hide_headers" + v := "X-Custom-Header1" + pl, err := parseProxyLabel(makeLabel(alias, field), v) + if err != nil { + t.Errorf("expected err=nil, got %v", err) + } + if pl.Alias != alias { + t.Errorf("expected alias=%s, got %s", alias, pl.Alias) + } + if pl.Field != field { + t.Errorf("expected field=%s, got %s", field, pl.Field) + } + sGot, ok := pl.Value.([]string) + sWant := []string{"X-Custom-Header1"} + if !ok { + t.Error("value is not []string") + } + if !reflect.DeepEqual(sGot, sWant) { + t.Errorf("expected %q, got %q", sWant, sGot) + } +} + +func TestCommaSepProxyLabelMulti(t *testing.T) { + alias := "foo" + field := "hide_headers" + v := "X-Custom-Header1, X-Custom-Header2,X-Custom-Header3" + pl, err := parseProxyLabel(makeLabel(alias, field), v) + if err != nil { + t.Errorf("expected err=nil, got %v", err) + } + if pl.Alias != alias { + t.Errorf("expected alias=%s, got %s", alias, pl.Alias) + } + if pl.Field != field { + t.Errorf("expected field=%s, got %s", field, pl.Field) + } + sGot, ok := pl.Value.([]string) + sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"} + if !ok { + t.Error("value is not []string") + } + if !reflect.DeepEqual(sGot, sWant) { + t.Errorf("expected %q, got %q", sWant, sGot) + } +} diff --git a/src/go-proxy/httputil_mod.go b/src/go-proxy/reverse_proxy_mod.go similarity index 92% rename from src/go-proxy/httputil_mod.go rename to src/go-proxy/reverse_proxy_mod.go index 83c10dd..fab8a20 100644 --- a/src/go-proxy/httputil_mod.go +++ b/src/go-proxy/reverse_proxy_mod.go @@ -1,6 +1,6 @@ package main -// A small mod on net/http/httputils +// A small mod on net/http/httputil/reverseproxy.go // that doubled the performance import ( @@ -8,14 +8,12 @@ import ( "errors" "fmt" "io" - "log" "net" "net/http" "net/http/httptrace" "net/textproto" "net/url" "strings" - "time" "golang.org/x/net/http/httpguts" ) @@ -39,16 +37,16 @@ type ProxyRequest struct { // // SetURL rewrites the outbound Host header to match the target's host. // To preserve the inbound request's Host header (the default behavior -// of [NewSingleHostReverseProxy]): +// of [NewReverseProxy]): // // rewriteFunc := func(r *httputil.ProxyRequest) { // r.SetURL(url) // r.Out.Host = r.In.Host // } -func (r *ProxyRequest) SetURL(target *url.URL) { - rewriteRequestURL(r.Out, target) - r.Out.Host = "" -} +// func (r *ProxyRequest) SetURL(target *url.URL) { +// rewriteRequestURL(r.Out, target) +// r.Out.Host = "" +// } // SetXForwarded sets the X-Forwarded-For, X-Forwarded-Host, and // X-Forwarded-Proto headers of the outbound request. @@ -132,17 +130,17 @@ type ReverseProxy struct { // recognizes a response as a streaming response, or // if its ContentLength is -1; for such responses, writes // are flushed to the client immediately. - FlushInterval time.Duration + // FlushInterval time.Duration // ErrorLog specifies an optional logger for errors // that occur when attempting to proxy the request. // If nil, logging is done via the log package's standard logger. - ErrorLog *log.Logger + // ErrorLog *log.Logger // BufferPool optionally specifies a buffer pool to // get byte slices for use by io.CopyBuffer when // copying HTTP response bodies. - BufferPool BufferPool + // BufferPool BufferPool // ModifyResponse is an optional function that modifies the // Response from the backend. It is called if the backend @@ -203,18 +201,18 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) { return a.Path + b.Path, apath + bpath } -// NewSingleHostReverseProxy returns a new [ReverseProxy] that routes +// NewReverseProxy returns a new [ReverseProxy] that routes // URLs to the scheme, host, and base path provided in target. If the // target's path is "/base" and the incoming request was for "/dir", // the target request will be for /base/dir. // -// NewSingleHostReverseProxy does not rewrite the Host header. +// NewReverseProxy does not rewrite the Host header. // // To customize the ReverseProxy behavior beyond what -// NewSingleHostReverseProxy provides, use ReverseProxy directly +// NewReverseProxy provides, use ReverseProxy directly // with a Rewrite function. The ProxyRequest SetURL method // may be used to route the outbound request. (Note that SetURL, -// unlike NewSingleHostReverseProxy, rewrites the Host header +// unlike NewReverseProxy, rewrites the Host header // of the outbound request by default.) // // proxy := &ReverseProxy{ @@ -223,9 +221,34 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) { // r.Out.Host = r.In.Host // if desired // }, // } -func NewSingleHostReverseProxy(target *url.URL, transport *http.Transport) *ReverseProxy { +func NewReverseProxy(target *url.URL, transport *http.Transport, config *ProxyConfig) *ReverseProxy { + // check on init rather than on request + var setHeaders = func(r *http.Request) {} + var hideHeaders = func(r *http.Request) {} + if len(config.SetHeaders) > 0 { + setHeaders = func(r *http.Request) { + h := config.SetHeaders.Clone() + for k, vv := range h { + if k == "Host" { + r.Host = vv[0] + } else { + r.Header[k] = vv + } + } + } + } + if len(config.HideHeaders) > 0 { + hideHeaders = func(r *http.Request) { + for _, k := range config.HideHeaders { + r.Header.Del(k) + } + } + } return &ReverseProxy{Rewrite: func(pr *ProxyRequest) { rewriteRequestURL(pr.Out, target) + pr.SetXForwarded() + setHeaders(pr.Out) + hideHeaders(pr.Out) }, Transport: transport} } @@ -380,7 +403,7 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // Strip client-provided forwarding headers. // The Rewrite func may use SetXForwarded to set new values // for these or copy the previous values from the inbound request. - // outreq.Header.Del("Forwarded") + outreq.Header.Del("Forwarded") // outreq.Header.Del("X-Forwarded-For") // outreq.Header.Del("X-Forwarded-Host") // outreq.Header.Del("X-Forwarded-Proto") @@ -388,29 +411,27 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // NOTE: removed // Remove unparsable query parameters from the outbound request. // outreq.URL.RawQuery = cleanQueryParams(outreq.URL.RawQuery) - pr := &ProxyRequest{ In: req, Out: outreq, } - pr.SetXForwarded() // NOTE: added p.Rewrite(pr) outreq = pr.Out // NOTE: removed // } else { - // if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { - // // If we aren't the first proxy retain prior - // // X-Forwarded-For information as a comma+space - // // separated list and fold multiple headers into one. - // prior, ok := outreq.Header["X-Forwarded-For"] - // omit := ok && prior == nil // Issue 38079: nil now means don't populate the header - // if len(prior) > 0 { - // clientIP = strings.Join(prior, ", ") + ", " + clientIP - // } - // if !omit { - // outreq.Header.Set("X-Forwarded-For", clientIP) - // } + // if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { + // // If we aren't the first proxy retain prior + // // X-Forwarded-For information as a comma+space + // // separated list and fold multiple headers into one. + // prior, ok := outreq.Header["X-Forwarded-For"] + // omit := ok && prior == nil // Issue 38079: nil now means don't populate the header + // if len(prior) > 0 { + // clientIP = strings.Join(prior, ", ") + ", " + clientIP // } + // if !omit { + // outreq.Header.Set("X-Forwarded-For", clientIP) + // } + // } // } if _, ok := outreq.Header["User-Agent"]; !ok { @@ -637,11 +658,11 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // } func (p *ReverseProxy) logf(format string, args ...any) { - if p.ErrorLog != nil { - p.ErrorLog.Printf(format, args...) - } else { - hrlog.Printf(format, args...) - } + // if p.ErrorLog != nil { + // p.ErrorLog.Printf(format, args...) + // } else { + hrlog.Errorf(format, args...) + // } } // NOTE: removed diff --git a/src/go-proxy/reverse_proxy_mod_test.go b/src/go-proxy/reverse_proxy_mod_test.go new file mode 100644 index 0000000..182c044 --- /dev/null +++ b/src/go-proxy/reverse_proxy_mod_test.go @@ -0,0 +1,102 @@ +package main + +import ( + "net/http" + "net/url" + "os" + "reflect" + "testing" + "time" +) + +var proxyCfg ProxyConfig +var proxyUrl, _ = url.Parse("http://127.0.0.1:8181") +var proxyServer = NewServer(ServerOptions{ + Name: "proxy", + HTTPAddr: ":8080", + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + NewReverseProxy(proxyUrl, &http.Transport{}, &proxyCfg).ServeHTTP(w, r) + }), +}) + +var testServer = NewServer(ServerOptions{ + Name: "test", + HTTPAddr: ":8181", + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := r.Header + for k, vv := range h { + for _, v := range vv { + w.Header().Add(k, v) + } + } + w.WriteHeader(http.StatusOK) + }), +}) + +var httpClient = http.DefaultClient + +func TestMain(m *testing.M) { + proxyServer.Start() + testServer.Start() + time.Sleep(100 * time.Millisecond) + code := m.Run() + proxyServer.Stop() + testServer.Stop() + os.Exit(code) +} + +func TestSetHeader(t *testing.T) { + hWant := http.Header{"X-Test": []string{"foo", "bar"}, "X-Test2": []string{"baz"}} + proxyCfg = ProxyConfig{ + Alias: "test", + Scheme: "http", + Host: "127.0.0.1", + Port: "8181", + SetHeaders: hWant, + } + req, err := http.NewRequest("HEAD", "http://127.0.0.1:8080", nil) + if err != nil { + t.Fatal(err) + } + resp, err := httpClient.Do(req) + if err != nil { + t.Fatal(err) + } + hGot := resp.Header + t.Log("headers: ", hGot) + for k, v := range hWant { + if !reflect.DeepEqual(hGot[k], v) { + t.Errorf("header %s: expected %v, got %v", k, v, hGot[k]) + } + } +} + +func TestHideHeader(t *testing.T) { + hHide := []string{"X-Test", "X-Test2"} + proxyCfg = ProxyConfig{ + Alias: "test", + Scheme: "http", + Host: "127.0.0.1", + Port: "8181", + HideHeaders: hHide, + } + req, err := http.NewRequest("HEAD", "http://127.0.0.1:8080", nil) + for _, k := range hHide { + req.Header.Set(k, "foo") + } + if err != nil { + t.Fatal(err) + } + resp, err := httpClient.Do(req) + if err != nil { + t.Fatal(err) + } + hGot := resp.Header + t.Log("headers: ", hGot) + for _, v := range hHide { + _, ok := hGot[v] + if ok { + t.Errorf("header %s: expected hidden, got %v", v, hGot[v]) + } + } +} diff --git a/src/go-proxy/route.go b/src/go-proxy/route.go index 6873533..732a3d7 100755 --- a/src/go-proxy/route.go +++ b/src/go-proxy/route.go @@ -22,7 +22,6 @@ func NewRoute(cfg *ProxyConfig) (Route, error) { if err != nil { return nil, NewNestedErrorFrom(err).Subject(cfg.Alias) } - httpRoutes.Get(cfg.Alias).Add(cfg.Path, route) return route, nil } } @@ -43,4 +42,4 @@ func isStreamScheme(s string) bool { } } return false -} \ No newline at end of file +} diff --git a/src/go-proxy/stream_route.go b/src/go-proxy/stream_route.go index 43aedf0..6100ff8 100755 --- a/src/go-proxy/stream_route.go +++ b/src/go-proxy/stream_route.go @@ -48,10 +48,18 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) { var srcPort, dstPort string var srcScheme, dstScheme string + l := srlog.WithFields(logrus.Fields{ + "alias": config.Alias, + }) portSplit := strings.Split(config.Port, ":") if len(portSplit) != 2 { - cfgl.Warnf("invalid port %s, assuming it is target port", config.Port) - srcPort = "0" + l.Warnf( + `%s: invalid port %s, + assuming it is target port`, + config.Alias, + config.Port, + ) + srcPort = "0" // will assign later dstPort = config.Port } else { srcPort = portSplit[0] @@ -101,11 +109,7 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) { stopCh: make(chan struct{}, 1), connCh: make(chan interface{}), started: false, - l: srlog.WithFields(logrus.Fields{ - "alias": config.Alias, - // "src": fmt.Sprintf("%s://:%d", srcScheme, srcPortInt), - // "dst": fmt.Sprintf("%s://%s:%d", dstScheme, config.Host, dstPortInt), - }), + l: l, }, nil } @@ -235,4 +239,4 @@ func (route *StreamRouteBase) grHandleConnections() { // id -> target type StreamRoutes SafeMap[string, StreamRoute] -var streamRoutes StreamRoutes = NewSafeMapOf[StreamRoutes]() \ No newline at end of file +var streamRoutes StreamRoutes = NewSafeMapOf[StreamRoutes]() diff --git a/src/go-proxy/utils.go b/src/go-proxy/utils.go index 24acd8b..ca730eb 100755 --- a/src/go-proxy/utils.go +++ b/src/go-proxy/utils.go @@ -3,6 +3,7 @@ package main import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net" @@ -212,13 +213,17 @@ func setFieldFromSnake[T interface{}, VT interface{}](obj *T, field string, valu field = utils.snakeToPascal(field) prop := reflect.ValueOf(obj).Elem().FieldByName(field) if prop.Kind() == 0 { - return NewNestedError("unknown field").Subject(field) + return errors.New("unknown field") } prop.Set(reflect.ValueOf(value)) return nil } func validateYaml(schema *jsonschema.Schema, data []byte) error { + if noSchemaValidation { + return nil + } + var i interface{} err := yaml.Unmarshal(data, &i) diff --git a/src/go-proxy/watcher.go b/src/go-proxy/watcher.go index a14b73e..84d336f 100644 --- a/src/go-proxy/watcher.go +++ b/src/go-proxy/watcher.go @@ -1,7 +1,7 @@ package main import ( - "path" + "strings" "sync" "time" @@ -22,8 +22,6 @@ type Watcher interface { } type watcherBase struct { - name string // for log / error output - kind string // for log / error output onChange func() l logrus.FieldLogger sync.Mutex @@ -42,30 +40,44 @@ type dockerWatcher struct { wg sync.WaitGroup } -func newWatcher(kind string, name string, onChange func()) *watcherBase { +func (p *Provider) newWatcher() *watcherBase { return &watcherBase{ - kind: kind, - name: name, - onChange: onChange, - l: wlog.WithFields(logrus.Fields{"kind": kind, "name": name}), - } -} -func NewFileWatcher(p string, onChange func(), onDelete func()) Watcher { - return &fileWatcher{ - watcherBase: newWatcher("File", path.Base(p), onChange), - path: p, - onDelete: onDelete, + onChange: p.ReloadRoutes, + l: p.l, } } -func NewDockerWatcher(c *client.Client, onChange func()) Watcher { +func (p *Provider) NewFileWatcher() Watcher { + return &fileWatcher{ + watcherBase: p.newWatcher(), + path: p.GetFilePath(), + onDelete: p.StopAllRoutes, + } +} + +func (p *Provider) NewDockerWatcher(c *client.Client) Watcher { return &dockerWatcher{ - watcherBase: newWatcher("Docker", c.DaemonHost(), onChange), + watcherBase: p.newWatcher(), client: c, stopCh: make(chan struct{}, 1), } } +func (c *config) newWatcher() *watcherBase { + return &watcherBase{ + onChange: c.MustReload, + l: c.l, + } +} + +func (c *config) NewFileWatcher() Watcher { + return &fileWatcher{ + watcherBase: c.newWatcher(), + path: c.reader.(*FileReader).Path, + onDelete: func() { c.l.Fatal("config file deleted") }, + } +} + func (w *fileWatcher) Start() { w.Lock() defer w.Unlock() @@ -100,7 +112,7 @@ func (w *fileWatcher) Dispose() { func (w *dockerWatcher) Start() { w.Lock() defer w.Unlock() - dockerWatchMap.Set(w.name, w) + dockerWatchMap.Set(w.client.DaemonHost(), w) w.wg.Add(1) go w.watch() } @@ -114,7 +126,7 @@ func (w *dockerWatcher) Stop() { close(w.stopCh) w.wg.Wait() w.stopCh = nil - dockerWatchMap.Delete(w.name) + dockerWatchMap.Delete(w.client.DaemonHost()) } func (w *dockerWatcher) Dispose() { @@ -164,10 +176,10 @@ func watchFiles() { } switch { case event.Has(fsnotify.Write): - w.l.Info("file changed") + w.l.Info("file changed: ", event.Name) go w.onChange() case event.Has(fsnotify.Remove), event.Has(fsnotify.Rename): - w.l.Info("file renamed / deleted") + w.l.Info("file renamed / deleted: ", event.Name) go w.onDelete() } case err := <-fsWatcher.Errors: @@ -194,16 +206,20 @@ func (w *dockerWatcher) watch() { case <-w.stopCh: return case msg := <-msgChan: - w.l.Infof("container %s %s", msg.Actor.Attributes["name"], msg.Action) + containerName := msg.Actor.Attributes["name"] + if strings.HasPrefix(containerName, "buildx_buildkit_builder-") { + continue + } + w.l.Infof("container %s %s", containerName, msg.Action) go w.onChange() case err := <-errChan: switch { case client.IsErrConnectionFailed(err): - w.l.Error(NewNestedError("connection failed").Subject(w.name)) + w.l.Error("watcher: connection failed") case client.IsErrNotFound(err): - w.l.Error(NewNestedError("endpoint not found").Subject(w.name)) + w.l.Error("watcher: endpoint not found") default: - w.l.Error(NewNestedErrorFrom(err).Subject(w.name)) + w.l.Errorf("watcher: %v", err) } time.Sleep(1 * time.Second) msgChan, errChan = listen() diff --git a/version.txt b/version.txt index 5546bd2..c650d5a 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.4.7 \ No newline at end of file +0.4.8 \ No newline at end of file