diff --git a/.gitignore b/.gitignore index 749b6d8b..c6cd1810 100755 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,4 @@ config/** bin/go-proxy.bak logs/ -log/ - -config-editor/ \ No newline at end of file +log/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 13956003..5fce6111 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,14 +1,16 @@ { "go.inferGopath": false, "yaml.schemas": { - "https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [ - "config.example.yml", - "config.yml", - "file:///config/workspace/go-proxy/config.example.yml" - ], + // "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 d4a54764..50a36894 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,13 @@ FROM alpine:latest LABEL maintainer="yusing@6uo.me" -RUN apk add --no-cache bash tzdata +RUN apk add --no-cache tzdata RUN mkdir /app -COPY bin/go-proxy entrypoint.sh /app/ +COPY bin/go-proxy /app/ COPY templates/ /app/templates -COPY config.example.yml /app/config/config.yml +COPY schema/ /app/schema -RUN chmod +x /app/go-proxy /app/entrypoint.sh +RUN chmod +x /app/go-proxy ENV DOCKER_HOST unix:///var/run/docker.sock ENV GOPROXY_DEBUG 0 ENV GOPROXY_REDIRECT_HTTP 1 @@ -19,4 +19,4 @@ EXPOSE 443 EXPOSE 8443 WORKDIR /app -ENTRYPOINT /app/entrypoint.sh \ No newline at end of file +CMD ["/app/go-proxy"] \ No newline at end of file diff --git a/Makefile b/Makefile index 0be5a836..c7f9ad76 100755 --- a/Makefile +++ b/Makefile @@ -2,6 +2,11 @@ all: build quick-restart logs +init-config: + mkdir -p config certs + [ -f config/config.yml ] || cp config.example.yml config/config.yml + [ -f config/providers.yml ] || touch config/providers.yml + build: mkdir -p bin CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy src/go-proxy/*.go @@ -9,12 +14,6 @@ build: up: docker compose up -d --build go-proxy -quick-restart: # quick restart without restarting the container - docker cp bin/go-proxy go-proxy:/app/go-proxy - docker cp templates/* go-proxy:/app/templates - docker cp entrypoint.sh go-proxy:/app/entrypoint.sh - docker exec -d go-proxy bash /app/entrypoint.sh restart - restart: docker kill go-proxy docker compose up -d go-proxy diff --git a/README.md b/README.md index adb90468..a4c53aa8 100755 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ In the examples domain `x.y.z` is used, replace them with your domain - [How to use](#how-to-use) - [Binary](#binary) - [Docker](#docker) + - [Command-line args](#command-line-args) + - [Commands](#commands) - [Use JSON Schema in VSCode](#use-json-schema-in-vscode) - [Configuration](#configuration) - [Labels (docker)](#labels-docker) @@ -37,14 +39,15 @@ In the examples domain `x.y.z` is used, replace them with your domain - Fast (See [benchmarks](#benchmarks)) - Auto certificate obtaining and renewal (See [Config File](#config-file) and [Supported DNS Challenge Providers](#supported-dns-challenge-providers)) - Auto detect reverse proxies from docker +- Auto hot-reload on container `start` / `die` / `stop` or config file changes - Custom proxy entries with `config.yml` and additional provider files - Subdomain matching + Path matching **(domain name doesn't matter)** -- HTTP(s) proxy + TCP/UDP Proxy +- HTTP(s) proxy + TCP/UDP Proxy (UDP is _experimental_) - HTTP(s) round robin load balance support (same subdomain and path across different hosts) -- Auto hot-reload on container `start` / `die` / `stop` or config file changes -- Simple panel to see all reverse proxies and health available on port [panel_port_http] (http) and port [panel_port_https] (https) +- Simple panel to see all reverse proxies and health available on port 8080 (http) and port 8443 (https) ![panel screenshot](screenshots/panel.png) + - Config editor to edit config and provider files with validation **Validate and save file with Ctrl+S** @@ -53,13 +56,20 @@ In the examples domain `x.y.z` is used, replace them with your domain ## How to use -1. Download and extract the latest release (or clone the repository if you want to try out experimental features) +1. Download and extract the latest release (or clone the repository) -2. Copy `config.example.yml` to `config/config.yml` and modify the content to fit your needs +2. Call `make init-config` to init config file and provider file -3. (Optional) write your own `config/providers.yml` from `providers.example.yml` +3. Point your domain (i.e `y.z`) to your machine's IP address -4. See [Binary](#binary) or [docker](#docker) + - A Record: `*.y.z` -> `10.0.10.1` + - AAAA Record: `*.y.z` -> `::ffff:a00:a01` + +4. Start `go-proxy` (see [Binary](#binary) or [docker](#docker)) + +5. Start editing config files + - with text editor (i.e. Visual Studio Code) + - with web config editor by navigate to `ip:8080` ### Binary @@ -110,28 +120,38 @@ In the examples domain `x.y.z` is used, replace them with your domain 7. check the logs with `docker compose logs` or `make logs` to see if there is any error, check panel at [panel port] for active proxies +## Command-line args + +`go-proxy [command]` + +### Commands + +- empty: start proxy server +- validate: validate config and exit +- reload: force reload config and exit + ## Use JSON Schema in VSCode Modify `.vscode/settings.json` to fit your needs ```json { - "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", - ] - } + "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" + ] + } } ``` ## Configuration -With container name, no label needs to be added *(most of the time)*. +With container name, no label needs to be added _(most of the time)_. ### Labels (docker) @@ -229,13 +249,13 @@ To add more provider support (**CloudDNS** as an example): 1. Fork this repo, modify [autocert.go](src/go-proxy/autocert.go#L305) - ```go - var providersGenMap = map[string]ProviderGenerator{ - "cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig), - // add here, i.e. - "clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig), - } - ``` + ```go + var providersGenMap = map[string]ProviderGenerator{ + "cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig), + // add here, i.e. + "clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig), + } + ``` 2. Go to [https://go-acme.github.io/lego/dns/clouddns](https://go-acme.github.io/lego/dns/clouddns/) and check for required config @@ -243,24 +263,24 @@ To add more provider support (**CloudDNS** as an example): 4. Set required config in `config.yml` `autocert` -> `options` section - ```shell - # From https://go-acme.github.io/lego/dns/clouddns/ - CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \ - CLOUDDNS_EMAIL=you@example.com \ - CLOUDDNS_PASSWORD=b9841238feb177a84330f \ - lego --email you@example.com --dns clouddns --domains my.example.org run - ``` + ```shell + # From https://go-acme.github.io/lego/dns/clouddns/ + CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \ + CLOUDDNS_EMAIL=you@example.com \ + CLOUDDNS_PASSWORD=b9841238feb177a84330f \ + lego --email you@example.com --dns clouddns --domains my.example.org run + ``` - Should turn into: + Should turn into: - ```yaml - autocert: - ... - options: - client_id: bLsdFAks23429841238feb177a572aX - email: you@example.com - password: b9841238feb177a84330f - ``` + ```yaml + autocert: + ... + options: + client_id: bLsdFAks23429841238feb177a572aX + email: you@example.com + password: b9841238feb177a84330f + ``` 5. Run and test if it works 6. Commit and create pull request @@ -471,6 +491,3 @@ 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) - -[panel_port_http]: 8080 -[panel_port_https]: 8443 diff --git a/bin/go-proxy b/bin/go-proxy index 5ebb0a45..ca0c3b51 100755 Binary files a/bin/go-proxy and b/bin/go-proxy differ diff --git a/compose.example.yml b/compose.example.yml index 722113f5..30a55309 100755 --- a/compose.example.yml +++ b/compose.example.yml @@ -19,23 +19,25 @@ services: # - 20000:20100/tcp # - 20000:20100/udp volumes: + - ./config:/app/config + + # if local docker provider is used + # - /var/run/docker.sock:/var/run/docker.sock:ro + # use existing certificate # - /path/to/cert.pem:/app/certs/cert.crt:ro # - /path/to/privkey.pem:/app/certs/priv.key:ro - # use autocert feature + # store autocert obtained cert # - ./certs:/app/certs + + # workaround for "lookup: no such host" + # dns: + # - 127.0.0.1 - # if local docker provider is used (by default) - - /var/run/docker.sock:/var/run/docker.sock:ro - - # to use custom config and providers - # - ./config:/app/config - dns: - - 127.0.0.1 # workaround for "lookup: no such host" - extra_hosts: - # required if you use local docker provider and have containers in `host` network_mode - - host.docker.internal:host-gateway + # if you have container running in "host" network mode + # extra_hosts: + # - host.docker.internal:host-gateway logging: driver: 'json-file' options: diff --git a/config.example.yml b/config.example.yml index 816d4899..55d3bd83 100644 --- a/config.example.yml +++ b/config.example.yml @@ -1,25 +1,21 @@ -# uncomment to use autocert -autocert: # (optional, if you need autocert feature) - email: "user@domain.com" # (required) email for acme certificate - domains: # (required) - - "*.y.z" # domain for acme certificate, use wild card to allow all subdomains - provider: cloudflare # (required) dns challenge provider (string) - options: # provider specific options - auth_token: "YOUR_ZONE_API_TOKEN" +# Autocert (uncomment to enable) +# autocert: # (optional, if you need autocert feature) +# email: "user@domain.com" # (required) email for acme certificate +# domains: # (required) +# - "*.y.z" # domain for acme certificate, use wild card to allow all subdomains +# provider: cloudflare # (required) dns challenge provider (string) +# options: # provider specific options +# auth_token: "YOUR_ZONE_API_TOKEN" providers: local: kind: docker # for value format, see https://docs.docker.com/reference/cli/dockerd/ + # i.e. FROM_ENV, ssh://user@10.0.1.1:22, tcp://10.0.2.1:2375 value: FROM_ENV - # remote1: - # kind: docker - # value: ssh://user@10.0.1.1 - # remote2: - # kind: docker - # value: tcp://10.0.1.1:2375 - # provider1: - # kind: file - # value: provider1.yml - # provider2: - # kind: file - # value: provider2.yml \ No newline at end of file + providers: + kind: file + value: providers.yml + +# Fixed options (optional, non hot-reloadable) +# timeout_shutdown: 5 +# redirect_to_https: false \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index 06b6a752..00000000 --- a/entrypoint.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -if [ "$1" == "restart" ]; then - echo "restarting" - killall go-proxy -fi -if [ "$GOPROXY_DEBUG" == "1" ]; then - /app/go-proxy 2> log/go-proxy.log & - tail -f /dev/null -else - /app/go-proxy -fi \ No newline at end of file diff --git a/go.sum b/go.sum index a7a47875..39702f75 100755 --- a/go.sum +++ b/go.sum @@ -1,17 +1,9 @@ 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/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.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 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.86.0 h1:jEKN5VHNYNYtfDL2lUFLTRo+nOVNPFxpXTstVx0rqHI= -github.com/cloudflare/cloudflare-go v0.86.0/go.mod h1:wYW/5UP02TUfBToa/yKbQHV+r6h1NnJ1Je7XjuGM4Jw= -github.com/cloudflare/cloudflare-go v0.91.0 h1:L7IR+86qrZuEMSjGFg4cwRwtHqC8uCPmMUkP7BD4CPw= -github.com/cloudflare/cloudflare-go v0.91.0/go.mod h1:nUqvBUUDRxNzsDSQjbqUNWHEIYAoUlgRmcAzMKlFdKs= 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= @@ -19,8 +11,6 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v26.0.0+incompatible h1:90BKrx1a1HKYpSnnBFR6AgDq/FqkHxwlUyzJVPxD30I= @@ -68,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= @@ -89,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= @@ -98,15 +86,13 @@ 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= @@ -132,8 +118,6 @@ 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/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.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -147,10 +131,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ 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/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= @@ -165,8 +147,6 @@ 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.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/schema/config.schema.json b/schema/config.schema.json index 6cdc0ff5..f3d76407 100644 --- a/schema/config.schema.json +++ b/schema/config.schema.json @@ -117,7 +117,17 @@ ] } } + }, + "timeout_shutdown": { + "title": "Shutdown timeout (in seconds)", + "type": "integer", + "minimum": 0 + }, + "redirect_to_https": { + "title": "Redirect to HTTPS", + "type": "boolean" } }, - "additionalProperties": false + "additionalProperties": false, + "required": ["providers"] } diff --git a/schema/providers.schema.json b/schema/providers.schema.json index 0a279351..373ffece 100644 --- a/schema/providers.schema.json +++ b/schema/providers.schema.json @@ -1,7 +1,14 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", "title": "go-proxy providers file", + "anyOf": [ + { + "type":"object" + }, + { + "type":"null" + } + ], "patternProperties": { "^[a-zA-Z0-9_-]+$": { "title": "Proxy entry", diff --git a/src/go-proxy/args.go b/src/go-proxy/args.go new file mode 100644 index 00000000..16e1a241 --- /dev/null +++ b/src/go-proxy/args.go @@ -0,0 +1,38 @@ +package main + +import ( + "flag" + + "github.com/sirupsen/logrus" +) + +type Args struct { + Command string +} + +const ( + CommandStart = "" + CommandVerify = "verify" + CommandReload = "reload" +) + +var ValidCommands = []string{CommandStart, CommandVerify, CommandReload} + +func getArgs() Args { + var args Args + flag.Parse() + args.Command = flag.Arg(0) + if err := validateArgs(args.Command, ValidCommands); err != nil { + logrus.Fatal(err) + } + return args +} + +func validateArgs[T comparable](arg T, validArgs []T) error { + for _, v := range validArgs { + if arg == v { + return nil + } + } + return NewNestedError("invalid argument").Subjectf("%v", arg) +} diff --git a/src/go-proxy/autocert.go b/src/go-proxy/autocert.go index 94d08a35..0ac17b76 100644 --- a/src/go-proxy/autocert.go +++ b/src/go-proxy/autocert.go @@ -21,9 +21,9 @@ import ( "github.com/go-acme/lego/v4/registration" ) -type ProviderOptions = map[string]string -type ProviderGenerator = func(ProviderOptions) (challenge.Provider, error) -type CertExpiries = map[string]time.Time +type ProviderOptions map[string]string +type ProviderGenerator func(ProviderOptions) (challenge.Provider, error) +type CertExpiries map[string]time.Time type AutoCertConfig struct { Email string `json:"email"` diff --git a/src/go-proxy/config.go b/src/go-proxy/config.go index df846568..56dfdf16 100644 --- a/src/go-proxy/config.go +++ b/src/go-proxy/config.go @@ -3,12 +3,14 @@ package main import ( "os" "sync" + "time" "gopkg.in/yaml.v3" ) // commented out if unused type Config interface { + Value() configModel // Load() error MustLoad() GetAutoCertProvider() (AutoCertProvider, error) @@ -21,7 +23,13 @@ type Config interface { } func NewConfig(path string) Config { - cfg := &config{reader: &FileReader{Path: path}} + cfg := &config{ + m: &configModel{ + TimeoutShutdown: 3 * time.Second, + RedirectToHTTPS: false, + }, + reader: &FileReader{Path: path}, + } cfg.watcher = NewFileWatcher( path, cfg.MustReload, // OnChange @@ -35,6 +43,10 @@ func ValidateConfig(data []byte) error { return cfg.Load() } +func (cfg *config) Value() configModel { + return *cfg.m +} + func (cfg *config) Load(reader ...Reader) error { cfg.mutex.Lock() defer cfg.mutex.Unlock() @@ -170,12 +182,14 @@ func (cfg *config) StopWatching() { } type configModel struct { - Providers map[string]*Provider `yaml:",flow" json:"providers"` - AutoCert AutoCertConfig `yaml:",flow" json:"autocert"` + Providers map[string]*Provider `yaml:",flow" json:"providers"` + AutoCert AutoCertConfig `yaml:",flow" json:"autocert"` + TimeoutShutdown time.Duration `yaml:"timeout_shutdown" json:"timeout_shutdown"` + RedirectToHTTPS bool `yaml:"redirect_to_https" json:"redirect_to_https"` } type config struct { - m *configModel + m *configModel reader Reader watcher Watcher diff --git a/src/go-proxy/constants.go b/src/go-proxy/constants.go index bc967fbc..943c6199 100644 --- a/src/go-proxy/constants.go +++ b/src/go-proxy/constants.go @@ -147,6 +147,4 @@ var logLevel = func() logrus.Level { logrus.SetLevel(logrus.DebugLevel) } return logrus.GetLevel() -}() - -var redirectToHTTPS = os.Getenv("GOPROXY_REDIRECT_HTTP") != "0" && os.Getenv("GOPROXY_REDIRECT_HTTP") != "false" +}() \ No newline at end of file diff --git a/src/go-proxy/http_route.go b/src/go-proxy/http_route.go index 83f0ad66..bb2e8660 100755 --- a/src/go-proxy/http_route.go +++ b/src/go-proxy/http_route.go @@ -44,8 +44,8 @@ func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) { PathMode: config.PathMode, l: hrlog.WithFields(logrus.Fields{ "alias": config.Alias, - "path": config.Path, - "path_mode": config.PathMode, + // "path": config.Path, + // "path_mode": config.PathMode, }), } @@ -157,6 +157,6 @@ func (config *ProxyConfig) pathSubModResp(r *http.Response) error { } // alias -> (path -> routes) -type HTTPRoutes = SafeMap[string, pathPoolMap] +type HTTPRoutes SafeMap[string, pathPoolMap] var httpRoutes HTTPRoutes = NewSafeMapOf[HTTPRoutes](newPathPoolMap) diff --git a/src/go-proxy/main.go b/src/go-proxy/main.go index 442f5f2f..ce599e29 100755 --- a/src/go-proxy/main.go +++ b/src/go-proxy/main.go @@ -1,12 +1,13 @@ package main import ( - "flag" "net/http" "os" "os/signal" "runtime" + "sync" "syscall" + "time" "github.com/sirupsen/logrus" ) @@ -16,21 +17,27 @@ var cfg Config func main() { runtime.GOMAXPROCS(runtime.NumCPU()) - var verifyOnly bool - flag.BoolVar(&verifyOnly, "verify", false, "verify config without starting server") - flag.Parse() + args := getArgs() logrus.SetFormatter(&logrus.TextFormatter{ - ForceColors: true, - DisableColors: false, - FullTimestamp: true, + ForceColors: true, + DisableColors: false, + FullTimestamp: true, TimestampFormat: "01-02 15:04:05", }) + if args.Command == CommandReload { + err := utils.reloadServer() + if err != nil { + logrus.Fatal(err) + } + return + } + cfg = NewConfig(configPath) cfg.MustLoad() - if verifyOnly { + if args.Command == CommandVerify { logrus.Printf("config OK") return } @@ -63,7 +70,7 @@ func main() { HTTPAddr: ":80", HTTPSAddr: ":443", Handler: http.HandlerFunc(proxyHandler), - RedirectToHTTPS: redirectToHTTPS, + RedirectToHTTPS: cfg.Value().RedirectToHTTPS, }) panelServer = NewServer(ServerOptions{ Name: "panel", @@ -71,7 +78,7 @@ func main() { HTTPAddr: ":8080", HTTPSAddr: ":8443", Handler: panelHandler, - RedirectToHTTPS: redirectToHTTPS, + RedirectToHTTPS: cfg.Value().RedirectToHTTPS, }) proxyServer.Start() @@ -88,10 +95,32 @@ func main() { signal.Notify(sig, syscall.SIGHUP) <-sig - // cfg.StopWatching() - StopFSWatcher() - StopDockerWatcher() - cfg.StopProviders() - panelServer.Stop() - proxyServer.Stop() + logrus.Info("shutting down") + done := make(chan struct{}, 1) + + var wg sync.WaitGroup + wg.Add(3) + + go func() { + StopFSWatcher() + StopDockerWatcher() + cfg.StopProviders() + wg.Done() + }() + go func() { + panelServer.Stop() + proxyServer.Stop() + wg.Done() + }() + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + logrus.Info("shutdown complete") + case <-time.After(cfg.Value().TimeoutShutdown * time.Second): + logrus.Info("timeout waiting for shutdown") + } } diff --git a/src/go-proxy/panel.go b/src/go-proxy/panel.go index b9a2400e..55c2ed40 100755 --- a/src/go-proxy/panel.go +++ b/src/go-proxy/panel.go @@ -2,6 +2,7 @@ package main import ( "errors" + "fmt" "html/template" "net/http" "net/url" @@ -68,7 +69,7 @@ func panelCheckTargetHealth(w http.ResponseWriter, r *http.Request) { func panelConfigEditor(w http.ResponseWriter, r *http.Request) { cfgFiles := make([]string, 0) cfgFiles = append(cfgFiles, path.Base(configPath)) - for _, p := range cfg.(*config).m.Providers { + for _, p := range cfg.Value().Providers { if p.Kind != ProviderKind_File { continue } @@ -99,12 +100,20 @@ func panelConfigUpdate(w http.ResponseWriter, r *http.Request) { panelHandleErr(w, r, err) return } - err = os.WriteFile(path.Join(configBasePath, p), content, 0644) + p = path.Join(configBasePath, p) + _, err = os.Stat(p) + exists := !errors.Is(err, os.ErrNotExist) + err = os.WriteFile(p, content, 0644) if err != nil { panelHandleErr(w, r, NewNestedError("unable to write config file").With(err)) return } w.WriteHeader(http.StatusOK) + if !exists { + w.Write([]byte(fmt.Sprintf("Config file %s created, remember to add it to config.yml!", p))) + return + } + w.Write([]byte(fmt.Sprintf("Config file %s updated", p))) } func panelServeFile(w http.ResponseWriter, r *http.Request) { @@ -141,4 +150,4 @@ func panelHandleErr(w http.ResponseWriter, r *http.Request, err error, code ...i return } http.Error(w, err.Error(), http.StatusInternalServerError) -} \ No newline at end of file +} diff --git a/src/go-proxy/proxy_config.go b/src/go-proxy/proxy_config.go index 86fb10f9..c735ca65 100644 --- a/src/go-proxy/proxy_config.go +++ b/src/go-proxy/proxy_config.go @@ -15,8 +15,8 @@ type ProxyConfig struct { provider *Provider } -type ProxyConfigMap = map[string]ProxyConfig -type ProxyConfigSlice = []ProxyConfig +type ProxyConfigMap map[string]ProxyConfig +type ProxyConfigSlice []ProxyConfig func NewProxyConfig(provider *Provider) ProxyConfig { return ProxyConfig{ diff --git a/src/go-proxy/route.go b/src/go-proxy/route.go index 54a5b142..e1e5bc34 100755 --- a/src/go-proxy/route.go +++ b/src/go-proxy/route.go @@ -47,6 +47,6 @@ func isStreamScheme(s string) bool { } // id -> target -type StreamRoutes = SafeMap[string, StreamRoute] +type StreamRoutes SafeMap[string, StreamRoute] var streamRoutes StreamRoutes = NewSafeMapOf[StreamRoutes]() diff --git a/src/go-proxy/server.go b/src/go-proxy/server.go index d45e337d..1a74504f 100644 --- a/src/go-proxy/server.go +++ b/src/go-proxy/server.go @@ -31,11 +31,11 @@ type ServerOptions struct { } type LogrusWrapper struct { - l *logrus.Entry + *logrus.Entry } func (l LogrusWrapper) Write(b []byte) (int, error) { - return l.l.Logger.WriterLevel(logrus.ErrorLevel).Write(b) + return l.Logger.WriterLevel(logrus.ErrorLevel).Write(b) } func NewServer(opt ServerOptions) *Server { diff --git a/src/go-proxy/stream_route.go b/src/go-proxy/stream_route.go index 3d24237b..5efe5789 100755 --- a/src/go-proxy/stream_route.go +++ b/src/go-proxy/stream_route.go @@ -45,10 +45,8 @@ type StreamRouteBase struct { func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) { var streamType string = StreamType_TCP - var srcPort string - var dstPort string - var srcScheme string - var dstScheme string + var srcPort, dstPort string + var srcScheme, dstScheme string portSplit := strings.Split(config.Port, ":") if len(portSplit) != 2 { @@ -101,8 +99,8 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) { 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), + // "src": fmt.Sprintf("%s://:%d", srcScheme, srcPortInt), + // "dst": fmt.Sprintf("%s://%s:%d", dstScheme, config.Host, dstPortInt), }), }, nil } diff --git a/src/go-proxy/utils.go b/src/go-proxy/utils.go index 723829b9..24acd8b2 100755 --- a/src/go-proxy/utils.go +++ b/src/go-proxy/utils.go @@ -94,6 +94,18 @@ func (*Utils) healthCheckStream(scheme, host string) error { return nil } +func (*Utils) reloadServer() error { + resp, err := healthCheckHttpClient.Post("http://localhost:8080/reload", "", nil) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return NewNestedError("server reload failed").Subjectf("%d", resp.StatusCode) + } + return nil +} + func (*Utils) snakeToPascal(s string) string { toHyphenCamel := http.CanonicalHeaderKey(strings.ReplaceAll(s, "_", "-")) return strings.ReplaceAll(toHyphenCamel, "-", "") diff --git a/src/go-proxy/watcher.go b/src/go-proxy/watcher.go index 83006840..a14b73ea 100644 --- a/src/go-proxy/watcher.go +++ b/src/go-proxy/watcher.go @@ -89,7 +89,7 @@ func (w *fileWatcher) Stop() { fileWatchMap.Delete(w.path) err := fsWatcher.Remove(w.path) if err != nil { - w.l.WithField("action", "stop").Error(err) + w.l.Error(err) } } diff --git a/templates/config_editor/index.html b/templates/config_editor/index.html index 1eb40e48..ce93abff 100644 --- a/templates/config_editor/index.html +++ b/templates/config_editor/index.html @@ -18,6 +18,9 @@ {{$cfgFile}} {{- end}} +
  • + + +
  • diff --git a/templates/config_editor/index.js b/templates/config_editor/index.js index 39684efa..564b5170 100644 --- a/templates/config_editor/index.js +++ b/templates/config_editor/index.js @@ -11,25 +11,43 @@ let editor = CodeMirror(editorElement, { tabSize: 2 }); -function loadFile(fileName) { - if (fileName === undefined) { +function setCurrentFile(filename) { + let old_nav_item = document.getElementById(`file-${currentFile}`); + if (old_nav_item !== null) { + old_nav_item.classList.remove("active"); + } + currentFile = filename; + document.title = `${currentFile} - Config Editor`; + let new_nav_item = document.getElementById(`file-${currentFile}`); + if (new_nav_item === null) { + new_file_btn = document.getElementById("new-file"); + file_list = document.getElementById("file-list"); + new_nav_item = document.createElement("li"); + new_nav_item.id = `file-${currentFile}`; + new_nav_item.innerHTML = `${currentFile}`; + file_list.insertBefore(new_nav_item, new_file_btn); + } + new_nav_item.classList.add("active"); +} + +function loadFile(filename) { + if (filename === undefined) { + return; + } + if (filename === '+') { + newFile(); return; } let req = new XMLHttpRequest(); - req.open("GET", `/config/${fileName}`, true); + req.open("GET", `/config/${filename}`, true); req.onreadystatechange = function () { if (req.readyState == 4) { if (req.status == 200) { - let old_nav_item = document.getElementById(`file-${currentFile}`); - old_nav_item.classList.remove("active"); editor.setValue(req.responseText); - currentFile = fileName; - let new_nav_item = document.getElementById(`file-${currentFile}`); - new_nav_item.classList.add("active"); - document.title = `${currentFile} - Config Editor`; + setCurrentFile(filename); console.log(`loaded ${currentFile}`); } else { - let msg = `Failed to load ${fileName}: ` + req.responseText; + let msg = `Failed to load ${filename}: ` + req.responseText; alert(msg); console.log(msg); } @@ -46,14 +64,35 @@ function saveFile(filename, content) { req.onreadystatechange = function () { if (req.readyState == 4) { if (req.status == 200) { - alert("Saved " + filename); + alert(req.responseText); } else { - alert("Error: " + req.responseText); + alert("Error:\n" + req.responseText); } } }; } +function newFile() { + let filename = prompt("Enter filename:"); + if (filename === undefined || filename === "") { + alert("File name cannot be empty"); + return; + } + if (!filename.endsWith(".yml") && !filename.endsWith(".yaml")) { + alert("File name must end with .yml or .yaml"); + return; + } + let files = document.getElementById("file-list").children; + for (let i = 0; i < files.length; i++) { + if (files[i].id === `file-${filename}`) { + alert("File already exists"); + return; + } + } + editor.setValue(""); + setCurrentFile(filename); +} + editor.setSize("100wh", "100vh"); editor.setOption("extraKeys", { Tab: function (cm) { diff --git a/templates/config_editor/style.css b/templates/config_editor/style.css index 9f56d526..8ed2ce69 100644 --- a/templates/config_editor/style.css +++ b/templates/config_editor/style.css @@ -36,6 +36,10 @@ body { padding-right: 4em; display: block; } +#new-file { + color: #f8f8f2 !important; + font-weight: bold; +} .active { font-weight: bold; background: rgba(255, 255, 255, 0.1);