mirror of
https://github.com/yusing/godoxy.git
synced 2025-07-06 14:34:04 +02:00
preparing for v0.5
This commit is contained in:
parent
24778d1093
commit
93359110a2
115 changed files with 5153 additions and 4395 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -3,8 +3,14 @@ compose.yml
|
||||||
config/
|
config/
|
||||||
certs/
|
certs/
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
templates/codemirror/
|
templates/codemirror/
|
||||||
|
|
||||||
logs/
|
logs/
|
||||||
log/
|
log/
|
||||||
|
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
!src/config/
|
13
Dockerfile
13
Dockerfile
|
@ -3,17 +3,15 @@ RUN apk add --no-cache unzip wget make
|
||||||
COPY Makefile .
|
COPY Makefile .
|
||||||
RUN make setup-codemirror
|
RUN make setup-codemirror
|
||||||
|
|
||||||
FROM golang:1.22.2-alpine as builder
|
FROM golang:1.22.4-alpine as builder
|
||||||
COPY src/ /src
|
COPY src /src
|
||||||
COPY go.mod go.sum /src/go-proxy
|
ENV GOCACHE=/root/.cache/go-build
|
||||||
WORKDIR /src/go-proxy
|
WORKDIR /src
|
||||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||||
go mod download
|
go mod download
|
||||||
|
|
||||||
ENV GOCACHE=/root/.cache/go-build
|
|
||||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||||
--mount=type=cache,target="/root/.cache/go-build" \
|
--mount=type=cache,target="/root/.cache/go-build" \
|
||||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o go-proxy
|
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o go-proxy github.com/yusing/go-proxy
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
|
@ -33,7 +31,6 @@ ENV GOPROXY_DEBUG 0
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
EXPOSE 443
|
EXPOSE 443
|
||||||
EXPOSE 8443
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
CMD ["/app/go-proxy"]
|
CMD ["/app/go-proxy"]
|
23
Makefile
23
Makefile
|
@ -16,10 +16,10 @@ setup-codemirror:
|
||||||
|
|
||||||
build:
|
build:
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy src/go-proxy/*.go
|
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy github.com/yusing/go-proxy
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test src/go-proxy/*.go
|
cd src && go test && cd ..
|
||||||
|
|
||||||
up:
|
up:
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
@ -28,22 +28,19 @@ restart:
|
||||||
docker compose restart -t 0
|
docker compose restart -t 0
|
||||||
|
|
||||||
logs:
|
logs:
|
||||||
tail -f log/go-proxy.log
|
docker compose logs -f
|
||||||
|
|
||||||
get:
|
get:
|
||||||
go get -d -u ./src/go-proxy
|
cd src && go get -u && go mod tidy && cd ..
|
||||||
|
|
||||||
|
debug:
|
||||||
|
make build && GOPROXY_DEBUG=1 bin/go-proxy
|
||||||
|
|
||||||
|
archive:
|
||||||
|
git archive HEAD -o ../go-proxy-$$(date +"%Y%m%d%H%M").zip
|
||||||
|
|
||||||
repush:
|
repush:
|
||||||
git reset --soft HEAD^
|
git reset --soft HEAD^
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "repush"
|
git commit -m "repush"
|
||||||
git push gitlab dev --force
|
git push gitlab dev --force
|
||||||
|
|
||||||
udp-server:
|
|
||||||
docker run -it --rm \
|
|
||||||
-p 9999:9999/udp \
|
|
||||||
--label proxy.test-udp.scheme=udp \
|
|
||||||
--label proxy.test-udp.port=20003:9999 \
|
|
||||||
--network host \
|
|
||||||
--name test-udp \
|
|
||||||
$$(docker build -q -f udp-test-server.Dockerfile .)
|
|
||||||
|
|
331
README.md
331
README.md
|
@ -1,176 +1,94 @@
|
||||||
# go-proxy
|
# go-proxy
|
||||||
|
|
||||||
A simple auto docker reverse proxy for home use. **Written in _Go_**
|
A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse proxy and load balancer with a web UI.
|
||||||
|
|
||||||
In the examples domain `x.y.z` is used, replace them with your domain
|
**Table of content**
|
||||||
|
|
||||||
## Table of content
|
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
- [Table of content](#table-of-content)
|
|
||||||
- [Key Points](#key-points)
|
- [Key Points](#key-points)
|
||||||
- [How to use](#how-to-use)
|
- [Getting Started](#getting-started)
|
||||||
- [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)
|
- [Commands](#commands)
|
||||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
- [Environment variables](#environment-variables)
|
||||||
- [Environment variables](#environment-variables)
|
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
||||||
- [Config File](#config-file)
|
- [Config File](#config-file)
|
||||||
- [Fields](#fields)
|
|
||||||
- [Provider Kinds](#provider-kinds)
|
|
||||||
- [Provider File](#provider-file)
|
- [Provider File](#provider-file)
|
||||||
- [Supported DNS Challenge Providers](#supported-dns-challenge-providers)
|
|
||||||
- [Troubleshooting](#troubleshooting)
|
|
||||||
- [Benchmarks](#benchmarks)
|
|
||||||
- [Known issues](#known-issues)
|
- [Known issues](#known-issues)
|
||||||
- [Memory usage](#memory-usage)
|
|
||||||
- [Build it yourself](#build-it-yourself)
|
- [Build it yourself](#build-it-yourself)
|
||||||
<!-- /TOC -->
|
<!-- /TOC -->
|
||||||
|
|
||||||
## Key Points
|
## Key Points
|
||||||
|
|
||||||
- Fast (See [benchmarks](#benchmarks))
|
- Easy to use
|
||||||
- Auto certificate obtaining and renewal (See [Config File](#config-file) and [Supported DNS Challenge Providers](#supported-dns-challenge-providers))
|
- Auto certificate obtaining and renewal (See [Supported DNS Challenge Providers](docs/dns_providers.md))
|
||||||
- Auto detect reverse proxies from docker
|
- Auto configuration for docker contaienrs
|
||||||
- Auto hot-reload on container `start` / `die` / `stop` or config file changes
|
- Auto hot-reload on container state / config file changes
|
||||||
- Custom proxy entries with `config.yml` and additional provider files
|
- Support HTTP(s), TCP and UDP
|
||||||
- Subdomain matching + Path matching **(domain name doesn't matter)**
|
- Support HTTP(s) round robin load balancing
|
||||||
- HTTP(s) reverse proxy + TCP/UDP Proxy
|
- Web UI for configuration and monitoring (See [screenshots](screeenshots))
|
||||||
- HTTP(s) round robin load balance support (same subdomain and path across different hosts)
|
- Written in **[Go](https://go.dev)**
|
||||||
- Web UI on port 8080 (http) and port 8443 (https)
|
|
||||||
|
|
||||||
- a simple panel to see all reverse proxies and health
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- a config editor to edit config and provider files with validation
|
|
||||||
|
|
||||||
**Validate and save file with Ctrl+S**
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|
||||||
## How to use
|
## Getting Started
|
||||||
|
|
||||||
1. Setup DNS Records to your machine's IP address
|
1. Setup DNS Records
|
||||||
|
|
||||||
- A Record: `*.y.z` -> `10.0.10.1`
|
- A Record: `*.y.z` -> `10.0.10.1`
|
||||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||||
|
|
||||||
2. Start `go-proxy` by
|
2. Start `go-proxy`
|
||||||
|
|
||||||
- [Running from binary or as a system service](docs/binary.md)
|
- [Binary / systemd service](docs/binary.md)
|
||||||
- [Running as a docker container](docs/docker.md)
|
- [Docker](docs/docker.md)
|
||||||
|
|
||||||
3. Start editing config files
|
3. Configure `go-proxy`
|
||||||
- with text editor (i.e. Visual Studio Code)
|
- with text editor (i.e. Visual Studio Code)
|
||||||
- or with web config editor by navigate to `http://ip:8080`
|
- or with web config editor via `http://ip:8080`
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|
||||||
## Tested Services
|
|
||||||
|
|
||||||
### HTTP/HTTPs Reverse Proxy
|
|
||||||
|
|
||||||
- Nginx
|
|
||||||
- Minio
|
|
||||||
- AdguardHome Dashboard
|
|
||||||
- etc.
|
|
||||||
|
|
||||||
### TCP Proxy
|
|
||||||
|
|
||||||
- Minecraft server
|
|
||||||
- PostgreSQL
|
|
||||||
- MariaDB
|
|
||||||
|
|
||||||
### UDP Proxy
|
|
||||||
|
|
||||||
- Adguardhome DNS
|
|
||||||
- Palworld Dedicated Server
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Command-line args
|
|
||||||
|
|
||||||
`go-proxy [command]`
|
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
- empty: start proxy server
|
- `go-proxy` start proxy server
|
||||||
- validate: validate config and exit
|
- `go-proxy validate` validate config and exit
|
||||||
- reload: trigger a force reload of config
|
- `go-proxy reload` trigger a force reload of config
|
||||||
|
|
||||||
Examples:
|
**For docker containers, run `docker exec -it go-proxy /app/go-proxy <command>`**
|
||||||
|
|
||||||
- Binary: `go-proxy reload`
|
### Environment variables
|
||||||
- Docker: `docker exec -it go-proxy /app/go-proxy reload`
|
|
||||||
|
Booleans:
|
||||||
|
|
||||||
|
- `GOPROXY_DEBUG` enable debug behaviors
|
||||||
|
- `GOPROXY_NO_SCHEMA_VALIDATION`: disable schema validation **(useful for testing new DNS Challenge providers)**
|
||||||
|
|
||||||
|
### Use JSON Schema in VSCode
|
||||||
|
|
||||||
|
Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscode/settings.json` and modify it to fit your needs
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|
||||||
## Use JSON Schema in VSCode
|
### Config File
|
||||||
|
|
||||||
Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscode/settings.json` and modify 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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## 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)**
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Config File
|
|
||||||
|
|
||||||
See [config.example.yml](config.example.yml) for more
|
See [config.example.yml](config.example.yml) for more
|
||||||
|
|
||||||
### Fields
|
```yaml
|
||||||
|
# autocert configuration
|
||||||
- `autocert`: autocert configuration
|
autocert:
|
||||||
|
email: # ACME Email
|
||||||
- `email`: ACME Email
|
domains: # a list of domains for cert registration
|
||||||
- `domains`: a list of domains for cert registration
|
provider: # DNS Challenge provider
|
||||||
- `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)
|
- ...
|
||||||
|
# reverse proxy providers configuration
|
||||||
- `providers`: reverse proxy providers configuration
|
providers:
|
||||||
- `kind`: provider kind (string), see [Provider Kinds](#provider-kinds)
|
entry_1:
|
||||||
- `value`: provider specific value
|
kind: docker
|
||||||
|
value: # `FROM_ENV` or full url to docker host
|
||||||
[🔼Back to top](#table-of-content)
|
entry_2:
|
||||||
|
kind: file
|
||||||
### Provider Kinds
|
value: # relative path of file to `config/`
|
||||||
|
```
|
||||||
- `docker`: load reverse proxies from docker
|
|
||||||
|
|
||||||
values:
|
|
||||||
|
|
||||||
- `FROM_ENV`: value from environment (`DOCKER_HOST`)
|
|
||||||
- full url to docker host (i.e. `tcp://host:2375`)
|
|
||||||
|
|
||||||
- `file`: load reverse proxies from provider file
|
|
||||||
|
|
||||||
value: relative path of file to `config/`
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|
||||||
|
@ -182,155 +100,12 @@ See [providers.example.yml](providers.example.yml) for examples
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|
||||||
### Supported DNS Challenge Providers
|
|
||||||
|
|
||||||
- Cloudflare
|
|
||||||
|
|
||||||
- `auth_token`: your zone API token
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
Q: How to fix when it shows "no matching route for subdomain \<subdomain>"?
|
|
||||||
|
|
||||||
A: Make sure the container is running, and \<subdomain> matches any container name / alias
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Benchmarks
|
|
||||||
|
|
||||||
Benchmarked with `wrk` connecting `traefik/whoami`'s `/bench` endpoint
|
|
||||||
|
|
||||||
Remote benchmark (client running wrk and `go-proxy` server are different devices)
|
|
||||||
|
|
||||||
- Direct connection
|
|
||||||
|
|
||||||
```shell
|
|
||||||
root@yusing-pc:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.100.3:8003/bench
|
|
||||||
Running 10s test @ http://10.0.100.3:8003/bench
|
|
||||||
10 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 94.75ms 199.92ms 1.68s 91.27%
|
|
||||||
Req/Sec 4.24k 1.79k 18.79k 72.13%
|
|
||||||
Latency Distribution
|
|
||||||
50% 1.14ms
|
|
||||||
75% 120.23ms
|
|
||||||
90% 245.63ms
|
|
||||||
99% 1.03s
|
|
||||||
423444 requests in 10.10s, 50.88MB read
|
|
||||||
Socket errors: connect 0, read 0, write 0, timeout 29
|
|
||||||
Requests/sec: 41926.32
|
|
||||||
Transfer/sec: 5.04MB
|
|
||||||
```
|
|
||||||
|
|
||||||
- With reverse proxy
|
|
||||||
|
|
||||||
```shell
|
|
||||||
root@yusing-pc:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.1.7/bench
|
|
||||||
Running 10s test @ http://10.0.1.7/bench
|
|
||||||
10 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 79.35ms 169.79ms 1.69s 92.55%
|
|
||||||
Req/Sec 4.27k 1.90k 19.61k 75.81%
|
|
||||||
Latency Distribution
|
|
||||||
50% 1.12ms
|
|
||||||
75% 105.66ms
|
|
||||||
90% 200.22ms
|
|
||||||
99% 814.59ms
|
|
||||||
409836 requests in 10.10s, 49.25MB read
|
|
||||||
Socket errors: connect 0, read 0, write 0, timeout 18
|
|
||||||
Requests/sec: 40581.61
|
|
||||||
Transfer/sec: 4.88MB
|
|
||||||
```
|
|
||||||
|
|
||||||
Local benchmark (client running wrk and `go-proxy` server are under same proxmox host but different LXCs)
|
|
||||||
|
|
||||||
- Direct connection
|
|
||||||
|
|
||||||
```shell
|
|
||||||
root@http-benchmark-client:~# wrk -t 10 -c 200 -d 10s --latency http://10.0.100.1/bench
|
|
||||||
Running 10s test @ http://10.0.100.1/bench
|
|
||||||
10 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 434.08us 539.35us 8.76ms 85.28%
|
|
||||||
Req/Sec 67.71k 6.31k 87.21k 71.20%
|
|
||||||
Latency Distribution
|
|
||||||
50% 153.00us
|
|
||||||
75% 646.00us
|
|
||||||
90% 1.18ms
|
|
||||||
99% 2.38ms
|
|
||||||
6739591 requests in 10.01s, 809.85MB read
|
|
||||||
Requests/sec: 673608.15
|
|
||||||
Transfer/sec: 80.94MB
|
|
||||||
```
|
|
||||||
|
|
||||||
- With `go-proxy` reverse proxy
|
|
||||||
|
|
||||||
```shell
|
|
||||||
root@http-benchmark-client:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.1.7/bench
|
|
||||||
Running 10s test @ http://10.0.1.7/bench
|
|
||||||
10 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 1.23ms 0.96ms 11.43ms 72.09%
|
|
||||||
Req/Sec 17.48k 1.76k 21.48k 70.20%
|
|
||||||
Latency Distribution
|
|
||||||
50% 0.98ms
|
|
||||||
75% 1.76ms
|
|
||||||
90% 2.54ms
|
|
||||||
99% 4.24ms
|
|
||||||
1739079 requests in 10.01s, 208.97MB read
|
|
||||||
Requests/sec: 173779.44
|
|
||||||
Transfer/sec: 20.88MB
|
|
||||||
```
|
|
||||||
|
|
||||||
- With `traefik-v3`
|
|
||||||
|
|
||||||
```shell
|
|
||||||
root@traefik-benchmark:~# wrk -t10 -c200 -d10s -H "Host: benchmark.whoami" --latency http://127.0.0.1:8000/bench
|
|
||||||
Running 10s test @ http://127.0.0.1:8000/bench
|
|
||||||
10 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 2.81ms 10.36ms 180.26ms 98.57%
|
|
||||||
Req/Sec 11.35k 1.74k 13.76k 85.54%
|
|
||||||
Latency Distribution
|
|
||||||
50% 1.59ms
|
|
||||||
75% 2.27ms
|
|
||||||
90% 3.17ms
|
|
||||||
99% 37.91ms
|
|
||||||
1125723 requests in 10.01s, 109.50MB read
|
|
||||||
Requests/sec: 112499.59
|
|
||||||
Transfer/sec: 10.94MB
|
|
||||||
```
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
- Cert "renewal" is actually obtaining a new cert instead of renewing the existing one
|
- Cert "renewal" is actually obtaining a new cert instead of renewing the existing one
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼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
|
## Build it yourself
|
||||||
|
|
||||||
1. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
|
1. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
|
||||||
|
|
|
@ -1,45 +1,20 @@
|
||||||
version: '3'
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: ghcr.io/yusing/go-proxy:latest
|
image: ghcr.io/yusing/go-proxy:latest
|
||||||
container_name: go-proxy
|
container_name: go-proxy
|
||||||
restart: always
|
restart: always
|
||||||
networks: # ^also add here
|
network_mode: host
|
||||||
- default
|
|
||||||
ports:
|
|
||||||
- 80:80 # http proxy
|
|
||||||
- 8080:8080 # http panel
|
|
||||||
# - 443:443 # optional, https proxy
|
|
||||||
# - 8443:8443 # optional, https panel
|
|
||||||
|
|
||||||
# optional, if you declared any tcp/udp proxy, set a range you want to use
|
|
||||||
# - 20000:20100/tcp
|
|
||||||
# - 20000:20100/udp
|
|
||||||
volumes:
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
|
|
||||||
# if local docker provider is used
|
# (Optional) choose one of below to enable https
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
# 1. use existing certificate
|
||||||
|
# if your cert is not named `cert.crt` change `cert_path` in `config/config.yml`
|
||||||
|
# if your cert key is not named `priv.key` change `key_path` in `config/config.yml`
|
||||||
|
|
||||||
# use existing certificate
|
# - /path/to/certs:/app/certs
|
||||||
# - /path/to/cert.pem:/app/certs/cert.crt:ro
|
|
||||||
# - /path/to/privkey.pem:/app/certs/priv.key:ro
|
# 2. use autocert, certs will be stored in ./certs (or other path you specify)
|
||||||
|
|
||||||
# store autocert obtained cert
|
|
||||||
# - ./certs:/app/certs
|
# - ./certs:/app/certs
|
||||||
|
|
||||||
# workaround for "lookup: no such host"
|
|
||||||
# dns:
|
|
||||||
# - 127.0.0.1
|
|
||||||
|
|
||||||
# if you have container running in "host" network mode
|
|
||||||
# extra_hosts:
|
|
||||||
# - host.docker.internal:host-gateway
|
|
||||||
logging:
|
|
||||||
driver: 'json-file'
|
|
||||||
options:
|
|
||||||
max-file: '1'
|
|
||||||
max-size: 128k
|
|
||||||
networks: # ^you may add other external networks
|
|
||||||
default:
|
|
||||||
driver: bridge
|
|
|
@ -1,21 +1,33 @@
|
||||||
# Autocert (uncomment to enable)
|
# Autocert (choose one below and uncomment to enable)
|
||||||
# autocert: # (optional, if you need autocert feature)
|
|
||||||
# email: "user@domain.com" # (required) email for acme certificate
|
# 1. use existing cert
|
||||||
# domains: # (required)
|
# autocert:
|
||||||
# - "*.y.z" # domain for acme certificate, use wild card to allow all subdomains
|
# provider: local
|
||||||
# provider: cloudflare # (required) dns challenge provider (string)
|
# cert_path: certs/cert.crt # optional, uncomment only if you need to change it
|
||||||
# options: # provider specific options
|
# key_path: certs/priv.key # optional, uncomment only if you need to change it
|
||||||
# auth_token: "YOUR_ZONE_API_TOKEN"
|
|
||||||
|
# 2. cloudflare
|
||||||
|
# autocert:
|
||||||
|
# provider: cloudflare
|
||||||
|
# email: # ACME Email
|
||||||
|
# domains: # a list of domains for cert registration
|
||||||
|
# -
|
||||||
|
# options:
|
||||||
|
# - auth_token: # your zone API token
|
||||||
|
|
||||||
|
# 3. other providers, check readme for more
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
local:
|
local:
|
||||||
kind: docker
|
kind: docker
|
||||||
# for value format, see https://docs.docker.com/reference/cli/dockerd/
|
# 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
|
# i.e. ssh://user@10.0.1.1:22, tcp://10.0.2.1:2375
|
||||||
|
# use FROM_ENV if you have binded docker socket to /var/run/docker.sock
|
||||||
value: FROM_ENV
|
value: FROM_ENV
|
||||||
providers:
|
providers:
|
||||||
kind: file
|
kind: file
|
||||||
value: providers.yml
|
value: providers.yml
|
||||||
|
|
||||||
# Fixed options (optional, non hot-reloadable)
|
# Fixed options (optional, non hot-reloadable)
|
||||||
|
|
||||||
# timeout_shutdown: 5
|
# timeout_shutdown: 5
|
||||||
# redirect_to_https: false
|
# redirect_to_https: false
|
104
docs/benchmark_result.md
Normal file
104
docs/benchmark_result.md
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# Benchmarks
|
||||||
|
|
||||||
|
Benchmarked with `wrk` and `traefik/whoami`'s `/bench` endpoint
|
||||||
|
|
||||||
|
## Remote benchmark
|
||||||
|
|
||||||
|
- Direct connection
|
||||||
|
|
||||||
|
```shell
|
||||||
|
root@yusing-pc:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.100.3:8003/bench
|
||||||
|
Running 10s test @ http://10.0.100.3:8003/bench
|
||||||
|
10 threads and 200 connections
|
||||||
|
Thread Stats Avg Stdev Max +/- Stdev
|
||||||
|
Latency 94.75ms 199.92ms 1.68s 91.27%
|
||||||
|
Req/Sec 4.24k 1.79k 18.79k 72.13%
|
||||||
|
Latency Distribution
|
||||||
|
50% 1.14ms
|
||||||
|
75% 120.23ms
|
||||||
|
90% 245.63ms
|
||||||
|
99% 1.03s
|
||||||
|
423444 requests in 10.10s, 50.88MB read
|
||||||
|
Socket errors: connect 0, read 0, write 0, timeout 29
|
||||||
|
Requests/sec: 41926.32
|
||||||
|
Transfer/sec: 5.04MB
|
||||||
|
```
|
||||||
|
|
||||||
|
- With reverse proxy
|
||||||
|
|
||||||
|
```shell
|
||||||
|
root@yusing-pc:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.1.7/bench
|
||||||
|
Running 10s test @ http://10.0.1.7/bench
|
||||||
|
10 threads and 200 connections
|
||||||
|
Thread Stats Avg Stdev Max +/- Stdev
|
||||||
|
Latency 79.35ms 169.79ms 1.69s 92.55%
|
||||||
|
Req/Sec 4.27k 1.90k 19.61k 75.81%
|
||||||
|
Latency Distribution
|
||||||
|
50% 1.12ms
|
||||||
|
75% 105.66ms
|
||||||
|
90% 200.22ms
|
||||||
|
99% 814.59ms
|
||||||
|
409836 requests in 10.10s, 49.25MB read
|
||||||
|
Socket errors: connect 0, read 0, write 0, timeout 18
|
||||||
|
Requests/sec: 40581.61
|
||||||
|
Transfer/sec: 4.88MB
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local benchmark (client running wrk and `go-proxy` server are under same proxmox host but different LXCs)
|
||||||
|
|
||||||
|
- Direct connection
|
||||||
|
|
||||||
|
```shell
|
||||||
|
root@http-benchmark-client:~# wrk -t 10 -c 200 -d 10s --latency http://10.0.100.1/bench
|
||||||
|
Running 10s test @ http://10.0.100.1/bench
|
||||||
|
10 threads and 200 connections
|
||||||
|
Thread Stats Avg Stdev Max +/- Stdev
|
||||||
|
Latency 434.08us 539.35us 8.76ms 85.28%
|
||||||
|
Req/Sec 67.71k 6.31k 87.21k 71.20%
|
||||||
|
Latency Distribution
|
||||||
|
50% 153.00us
|
||||||
|
75% 646.00us
|
||||||
|
90% 1.18ms
|
||||||
|
99% 2.38ms
|
||||||
|
6739591 requests in 10.01s, 809.85MB read
|
||||||
|
Requests/sec: 673608.15
|
||||||
|
Transfer/sec: 80.94MB
|
||||||
|
```
|
||||||
|
|
||||||
|
- With `go-proxy` reverse proxy
|
||||||
|
|
||||||
|
```shell
|
||||||
|
root@http-benchmark-client:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.1.7/bench
|
||||||
|
Running 10s test @ http://10.0.1.7/bench
|
||||||
|
10 threads and 200 connections
|
||||||
|
Thread Stats Avg Stdev Max +/- Stdev
|
||||||
|
Latency 1.23ms 0.96ms 11.43ms 72.09%
|
||||||
|
Req/Sec 17.48k 1.76k 21.48k 70.20%
|
||||||
|
Latency Distribution
|
||||||
|
50% 0.98ms
|
||||||
|
75% 1.76ms
|
||||||
|
90% 2.54ms
|
||||||
|
99% 4.24ms
|
||||||
|
1739079 requests in 10.01s, 208.97MB read
|
||||||
|
Requests/sec: 173779.44
|
||||||
|
Transfer/sec: 20.88MB
|
||||||
|
```
|
||||||
|
|
||||||
|
- With `traefik-v3`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
root@traefik-benchmark:~# wrk -t10 -c200 -d10s -H "Host: benchmark.whoami" --latency http://127.0.0.1:8000/bench
|
||||||
|
Running 10s test @ http://127.0.0.1:8000/bench
|
||||||
|
10 threads and 200 connections
|
||||||
|
Thread Stats Avg Stdev Max +/- Stdev
|
||||||
|
Latency 2.81ms 10.36ms 180.26ms 98.57%
|
||||||
|
Req/Sec 11.35k 1.74k 13.76k 85.54%
|
||||||
|
Latency Distribution
|
||||||
|
50% 1.59ms
|
||||||
|
75% 2.27ms
|
||||||
|
90% 3.17ms
|
||||||
|
99% 37.91ms
|
||||||
|
1125723 requests in 10.01s, 109.50MB read
|
||||||
|
Requests/sec: 112499.59
|
||||||
|
Transfer/sec: 10.94MB
|
||||||
|
```
|
|
@ -22,7 +22,7 @@
|
||||||
Setup
|
Setup
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
wget -qO- https://6uo.me/go-proxy-setup-binary | sudo bash
|
wget -qO- https://github.com/yusing/go-proxy/raw/main/setup-binary.sh | sudo bash
|
||||||
```
|
```
|
||||||
|
|
||||||
What it does:
|
What it does:
|
||||||
|
|
32
docs/dns_providers.md
Normal file
32
docs/dns_providers.md
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Supported DNS Providers
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
- [Cloudflare](#cloudflare)
|
||||||
|
- [CloudDNS](#clouddns)
|
||||||
|
- [DuckDNS](#duckdns)
|
||||||
|
- [Implement other DNS providers](#implement-other-dns-providers)
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
|
## Cloudflare
|
||||||
|
|
||||||
|
`auth_token` your zone API token
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
`token`: DuckDNS Token
|
||||||
|
|
||||||
|
Tested by [earvingad](https://github.com/earvingad)
|
||||||
|
|
||||||
|
## Implement other DNS providers
|
||||||
|
|
||||||
|
See [add_dns_provider.md](docs/add_dns_provider.md)
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
2. Run setup script
|
2. Run setup script
|
||||||
|
|
||||||
`bash <(wget -qO- https://6uo.me/go-proxy-setup-docker)`
|
`bash <(wget -qO- https://github.com/yusing/go-proxy/raw/main/setup-docker.sh)`
|
||||||
|
|
||||||
What it does:
|
What it does:
|
||||||
|
|
||||||
|
@ -75,19 +75,25 @@
|
||||||
|
|
||||||
- `proxy.*.<field>`: wildcard label for all aliases
|
- `proxy.*.<field>`: wildcard label for all aliases
|
||||||
|
|
||||||
Below labels has a **`proxy.<alias>.`** prefix (i.e. `proxy.nginx.scheme: http`)
|
_Labels below should have a **`proxy.<alias>.`** prefix._
|
||||||
|
|
||||||
|
_i.e. `proxy.nginx.scheme: http`_
|
||||||
|
|
||||||
- `scheme`: proxy protocol
|
- `scheme`: proxy protocol
|
||||||
- default: `http`
|
- default:
|
||||||
|
- if `port` is like `x:y`: `tcp`
|
||||||
|
- if `port` is a number: `http`
|
||||||
- allowed: `http`, `https`, `tcp`, `udp`
|
- allowed: `http`, `https`, `tcp`, `udp`
|
||||||
- `host`: proxy host
|
- `host`: proxy host
|
||||||
- default: `container_name`
|
- default: `container_name`
|
||||||
|
- allowed: IP address, hostname
|
||||||
- `port`: proxy port
|
- `port`: proxy port
|
||||||
- default: first expose port (declared in `Dockerfile` or `docker-compose.yml`)
|
- default: first port in `ports:`
|
||||||
- `http(s)`: number in range og `0 - 65535`
|
- `http(s)`: number in range og `0 - 65535`
|
||||||
- `tcp/udp`: `[<listeningPort>:]<targetPort>`
|
- `tcp`, `udp`: `x:y`
|
||||||
- `listeningPort`: number, when it is omitted (not suggested), a free port starting from 20000 will be used.
|
- `x`: port for `go-proxy` to listen on
|
||||||
- `targetPort`: number, or predefined names (see [constants.go:14](src/go-proxy/constants.go#L14))
|
- `y`: port, or _service name_ of target container
|
||||||
|
see [constants.go:14 for _service names_](../src/common/constants.go#L74)
|
||||||
- `no_tls_verify`: whether skip tls verify when scheme is https
|
- `no_tls_verify`: whether skip tls verify when scheme is https
|
||||||
- default: `false`
|
- default: `false`
|
||||||
- `path`: proxy path _(http(s) proxy only)_
|
- `path`: proxy path _(http(s) proxy only)_
|
||||||
|
@ -95,7 +101,7 @@ Below labels has a **`proxy.<alias>.`** prefix (i.e. `proxy.nginx.scheme: http`)
|
||||||
- `path_mode`: mode for path handling
|
- `path_mode`: mode for path handling
|
||||||
|
|
||||||
- default: empty
|
- default: empty
|
||||||
- allowed: empty, `forward`, `sub`
|
- allowed: empty, `forward`
|
||||||
|
|
||||||
- `empty`: remove path prefix from URL when proxying
|
- `empty`: remove path prefix from URL when proxying
|
||||||
1. apps.y.z/webdav -> webdav:80
|
1. apps.y.z/webdav -> webdav:80
|
||||||
|
@ -103,28 +109,24 @@ Below labels has a **`proxy.<alias>.`** prefix (i.e. `proxy.nginx.scheme: http`)
|
||||||
- `forward`: path remain unchanged
|
- `forward`: path remain unchanged
|
||||||
1. apps.y.z/webdav -> webdav:80/webdav
|
1. apps.y.z/webdav -> webdav:80/webdav
|
||||||
2. apps.y.z./webdav/path/to/file -> webdav:80/webdav/path/to/file
|
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)
|
- `set_headers`: a list of header to set, (key:value, one by line)
|
||||||
|
|
||||||
Duplicated keys will be treated as multiple-value headers
|
Duplicated keys will be treated as multiple-value headers
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
labels:
|
labels:
|
||||||
proxy.app.set_headers: |
|
proxy.app.set_headers: |
|
||||||
X-Custom-Header1: value1
|
X-Custom-Header1: value1
|
||||||
X-Custom-Header1: value2
|
X-Custom-Header1: value2
|
||||||
X-Custom-Header2: value2
|
X-Custom-Header2: value2
|
||||||
```
|
```
|
||||||
|
|
||||||
- `hide_headers`: comma seperated list of headers to hide
|
- `hide_headers`: comma seperated list of headers to hide
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|
||||||
## Labels (docker specific)
|
**docker only:**
|
||||||
|
|
||||||
Below labels has a **`proxy.<alias>.`** prefix (i.e. `proxy.app.load_balance=1`)
|
|
||||||
|
|
||||||
- `load_balance`: enable load balance
|
- `load_balance`: enable load balance
|
||||||
- allowed: `1`, `true`
|
- allowed: `1`, `true`
|
||||||
|
|
3
go.work
Normal file
3
go.work
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
use ./src
|
|
@ -7,9 +7,7 @@ example: # matching `app.y.z`
|
||||||
port: "80"
|
port: "80"
|
||||||
# optional, defaults to empty
|
# optional, defaults to empty
|
||||||
path:
|
path:
|
||||||
# optional, defaults to empty
|
# optional (scheme=https only)
|
||||||
path_mode:
|
|
||||||
# optional (https only)
|
|
||||||
# no_tls_verify: false
|
# no_tls_verify: false
|
||||||
# optional headers to set / override (http(s) only)
|
# optional headers to set / override (http(s) only)
|
||||||
set_headers:
|
set_headers:
|
||||||
|
@ -21,18 +19,9 @@ example: # matching `app.y.z`
|
||||||
hide_headers:
|
hide_headers:
|
||||||
- HEADER_C
|
- HEADER_C
|
||||||
- HEADER_D
|
- HEADER_D
|
||||||
app1: # matching `app1.y.z` -> http://x.y.z
|
app1: # matching `app1.y.z` -> http://some_host
|
||||||
host: x.y.z
|
host: some_host
|
||||||
app2: # `app2` has no effect for tcp / udp, but still has to be unique across files
|
app2:
|
||||||
scheme: tcp
|
scheme: tcp
|
||||||
host: 10.0.0.2
|
host: 10.0.0.2
|
||||||
port: 20000:tcp
|
port: 20000:tcp
|
||||||
app3: # matching `app3.y.z` -> https://10.0.0.1/app3
|
|
||||||
scheme: https
|
|
||||||
host: 10.0.0.1
|
|
||||||
path: /app3
|
|
||||||
path_mode: forward
|
|
||||||
no_tls_verify: false
|
|
||||||
set_headers:
|
|
||||||
X-Forwarded-Proto: [https]
|
|
||||||
X-Forwarded-Host: [app3.y.z]
|
|
||||||
|
|
|
@ -8,31 +8,63 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"email": {
|
"email": {
|
||||||
"description": "ACME Email",
|
"title": "ACME Email",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
|
"pattern": "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
|
||||||
"patternErrorMessage": "Invalid email"
|
"patternErrorMessage": "Invalid email"
|
||||||
},
|
},
|
||||||
"domains": {
|
"domains": {
|
||||||
"description": "Cert Domains",
|
"title": "Cert Domains",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"minItems": 1
|
"minItems": 1
|
||||||
},
|
},
|
||||||
|
"cert_path": {
|
||||||
|
"title": "path of cert file to load/store",
|
||||||
|
"description": "default: certs/cert.crt",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"key_path": {
|
||||||
|
"title": "path of key file to load/store",
|
||||||
|
"description": "default: certs/priv.key",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"provider": {
|
"provider": {
|
||||||
"description": "DNS Challenge Provider",
|
"title": "DNS Challenge Provider",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["cloudflare", "clouddns", "duckdns"]
|
"enum": [
|
||||||
|
"local",
|
||||||
|
"cloudflare",
|
||||||
|
"clouddns",
|
||||||
|
"duckdns"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"description": "Provider specific options",
|
"title": "Provider specific options",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["email", "domains", "provider", "options"],
|
|
||||||
"allOf": [
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"provider": {
|
||||||
|
"not": true,
|
||||||
|
"const": "local"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": [
|
||||||
|
"email",
|
||||||
|
"domains",
|
||||||
|
"provider",
|
||||||
|
"options"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"if": {
|
"if": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -44,7 +76,9 @@
|
||||||
"then": {
|
"then": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"options": {
|
"options": {
|
||||||
"required": ["auth_token"],
|
"required": [
|
||||||
|
"auth_token"
|
||||||
|
],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"auth_token": {
|
"auth_token": {
|
||||||
|
@ -67,7 +101,11 @@
|
||||||
"then": {
|
"then": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"options": {
|
"options": {
|
||||||
"required": ["client_id", "email", "password"],
|
"required": [
|
||||||
|
"client_id",
|
||||||
|
"email",
|
||||||
|
"password"
|
||||||
|
],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"client_id": {
|
"client_id": {
|
||||||
|
@ -98,7 +136,9 @@
|
||||||
"then": {
|
"then": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"options": {
|
"options": {
|
||||||
"required": ["token"],
|
"required": [
|
||||||
|
"token"
|
||||||
|
],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"token": {
|
"token": {
|
||||||
|
@ -123,13 +163,19 @@
|
||||||
"kind": {
|
"kind": {
|
||||||
"description": "Proxy provider kind",
|
"description": "Proxy provider kind",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["docker", "file"]
|
"enum": [
|
||||||
|
"docker",
|
||||||
|
"file"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["kind", "value"],
|
"required": [
|
||||||
|
"kind",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"if": {
|
"if": {
|
||||||
|
@ -190,5 +236,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["providers"]
|
"required": [
|
||||||
|
"providers"
|
||||||
|
]
|
||||||
}
|
}
|
|
@ -3,10 +3,10 @@
|
||||||
"title": "go-proxy providers file",
|
"title": "go-proxy providers file",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type":"object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type":"null"
|
"type": "null"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"patternProperties": {
|
"patternProperties": {
|
||||||
|
@ -19,11 +19,20 @@
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["http", "https", "tcp", "udp"]
|
"enum": [
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"tcp",
|
||||||
|
"udp",
|
||||||
|
"tcp:tcp",
|
||||||
|
"udp:udp",
|
||||||
|
"tcp:udp",
|
||||||
|
"udp:tcp"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "null",
|
"type": "null",
|
||||||
"description": "HTTP proxy"
|
"description": "Auto detect base on port number"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -50,8 +59,9 @@
|
||||||
"port": {
|
"port": {
|
||||||
"title": "Proxy port"
|
"title": "Proxy port"
|
||||||
},
|
},
|
||||||
"path": {},
|
"path": {
|
||||||
"path_mode": {},
|
"title": "Proxy path pattern (See https://pkg.go.dev/net/http#ServeMux)"
|
||||||
|
},
|
||||||
"no_tls_verify": {
|
"no_tls_verify": {
|
||||||
"description": "Disable TLS verification for https proxy",
|
"description": "Disable TLS verification for https proxy",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
@ -59,7 +69,9 @@
|
||||||
"set_headers": {},
|
"set_headers": {},
|
||||||
"hide_headers": {}
|
"hide_headers": {}
|
||||||
},
|
},
|
||||||
"required": ["host"],
|
"required": [
|
||||||
|
"host"
|
||||||
|
],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
|
@ -68,7 +80,10 @@
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"scheme": {
|
"scheme": {
|
||||||
"enum": ["http", "https"]
|
"enum": [
|
||||||
|
"http",
|
||||||
|
"https"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -119,19 +134,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"path_mode": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"description": "Proxy path mode (forward, sub, empty)",
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["", "forward", "sub"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Default proxy path mode (sub)",
|
|
||||||
"type": "null"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"set_headers": {
|
"set_headers": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Proxy headers to set",
|
"description": "Proxy headers to set",
|
||||||
|
@ -143,7 +145,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hide_headers": {
|
"hide_headers": {
|
||||||
"type":"array",
|
"type": "array",
|
||||||
"description": "Proxy headers to hide",
|
"description": "Proxy headers to hide",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -154,17 +156,14 @@
|
||||||
"else": {
|
"else": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"port": {
|
"port": {
|
||||||
"markdownDescription": "`listening port`:`target port | service type`",
|
"markdownDescription": "`listening port`:`proxy port | service name`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[0-9]+\\:[0-9a-z]+$",
|
"pattern": "^[0-9]+\\:[0-9a-z]+$",
|
||||||
"patternErrorMessage": "'port' must be in the format of '<listening port>:<target port | service type>'"
|
"patternErrorMessage": "'port' must be in the format of '<listening port>:<proxy port | service name>'"
|
||||||
},
|
},
|
||||||
"path": {
|
"path": {
|
||||||
"not": true
|
"not": true
|
||||||
},
|
},
|
||||||
"path_mode": {
|
|
||||||
"not": true
|
|
||||||
},
|
|
||||||
"set_headers": {
|
"set_headers": {
|
||||||
"not": true
|
"not": true
|
||||||
},
|
},
|
||||||
|
@ -172,7 +171,9 @@
|
||||||
"not": true
|
"not": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["port"]
|
"required": [
|
||||||
|
"port"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
29
src/api/handler.go
Normal file
29
src/api/handler.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
v1 "github.com/yusing/go-proxy/api/v1"
|
||||||
|
"github.com/yusing/go-proxy/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewHandler(cfg *config.Config) http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("GET /v1", v1.Index)
|
||||||
|
mux.HandleFunc("GET /v1/checkhealth", wrap(cfg, v1.CheckHealth))
|
||||||
|
mux.HandleFunc("HEAD /v1/checkhealth", wrap(cfg, v1.CheckHealth))
|
||||||
|
mux.HandleFunc("POST /v1/reload", wrap(cfg, v1.Reload))
|
||||||
|
mux.HandleFunc("GET /v1/list", wrap(cfg, v1.List))
|
||||||
|
mux.HandleFunc("GET /v1/list/{what}", wrap(cfg, v1.List))
|
||||||
|
mux.HandleFunc("GET /v1/file", v1.GetFileContent)
|
||||||
|
mux.HandleFunc("GET /v1/file/{filename}", v1.GetFileContent)
|
||||||
|
mux.HandleFunc("PUT /v1/file/{filename}", v1.SetFileContent)
|
||||||
|
mux.HandleFunc("GET /v1/stats", wrap(cfg, v1.Stats))
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrap(cfg *config.Config, f func(cfg *config.Config, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
f(cfg, w, r)
|
||||||
|
}
|
||||||
|
}
|
49
src/api/v1/checkhealth.go
Normal file
49
src/api/v1/checkhealth.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
U "github.com/yusing/go-proxy/api/v1/utils"
|
||||||
|
"github.com/yusing/go-proxy/config"
|
||||||
|
R "github.com/yusing/go-proxy/route"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CheckHealth(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
|
target := r.FormValue("target")
|
||||||
|
if target == "" {
|
||||||
|
U.HandleErr(w, r, U.ErrMissingKey("target"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
switch route := cfg.FindRoute(target).(type) {
|
||||||
|
case nil:
|
||||||
|
U.HandleErr(w, r, U.ErrNotFound("target", target), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
case *R.HTTPRoute:
|
||||||
|
path := r.FormValue("path")
|
||||||
|
if path == "" {
|
||||||
|
U.HandleErr(w, r, U.ErrMissingKey("path"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sr, hasSr := route.GetSubroute(path)
|
||||||
|
if !hasSr {
|
||||||
|
U.HandleErr(w, r, U.ErrNotFound("path", path), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ok = U.IsSiteHealthy(sr.TargetURL.String())
|
||||||
|
case *R.StreamRoute:
|
||||||
|
ok = U.IsStreamHealthy(
|
||||||
|
route.Scheme.ProxyScheme.String(),
|
||||||
|
fmt.Sprintf("%s:%v", route.Host, route.Port.ProxyPort),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusRequestTimeout)
|
||||||
|
}
|
||||||
|
}
|
58
src/api/v1/file.go
Normal file
58
src/api/v1/file.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
U "github.com/yusing/go-proxy/api/v1/utils"
|
||||||
|
"github.com/yusing/go-proxy/common"
|
||||||
|
"github.com/yusing/go-proxy/config"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
"github.com/yusing/go-proxy/proxy/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filename := r.PathValue("filename")
|
||||||
|
if filename == "" {
|
||||||
|
filename = common.ConfigFileName
|
||||||
|
}
|
||||||
|
content, err := os.ReadFile(path.Join(common.ConfigBasePath, filename))
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filename := r.PathValue("filename")
|
||||||
|
if filename == "" {
|
||||||
|
U.HandleErr(w, r, U.ErrMissingKey("filename"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content := make([]byte, r.ContentLength)
|
||||||
|
_, err := E.Check(r.Body.Read(content))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
U.HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if filename == common.ConfigFileName {
|
||||||
|
err = config.Validate(content)
|
||||||
|
} else {
|
||||||
|
err = provider.Validate(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.IsNotNil() {
|
||||||
|
U.HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = E.From(os.WriteFile(path.Join(common.ConfigBasePath, filename), content, 0644))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
U.HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
7
src/api/v1/index.go
Normal file
7
src/api/v1/index.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func Index(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("API ready"))
|
||||||
|
}
|
62
src/api/v1/list.go
Normal file
62
src/api/v1/list.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/common"
|
||||||
|
"github.com/yusing/go-proxy/config"
|
||||||
|
|
||||||
|
U "github.com/yusing/go-proxy/api/v1/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func List(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
|
what := r.PathValue("what")
|
||||||
|
if what == "" {
|
||||||
|
what = "routes"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch what {
|
||||||
|
case "routes":
|
||||||
|
listRoutes(cfg, w, r)
|
||||||
|
case "config_files":
|
||||||
|
listConfigFiles(w, r)
|
||||||
|
default:
|
||||||
|
U.HandleErr(w, r, U.ErrInvalidKey("what"), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listRoutes(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
|
routes := cfg.RoutesByAlias()
|
||||||
|
type_filter := r.FormValue("type")
|
||||||
|
if type_filter != "" {
|
||||||
|
for k, v := range routes {
|
||||||
|
if v["type"] != type_filter {
|
||||||
|
delete(routes, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := U.RespondJson(routes, w); err != nil {
|
||||||
|
U.HandleErr(w, r, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listConfigFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
files, err := os.ReadDir(common.ConfigBasePath)
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filenames := make([]string, len(files))
|
||||||
|
for i, f := range files {
|
||||||
|
filenames[i] = f.Name()
|
||||||
|
}
|
||||||
|
resp, err := json.Marshal(filenames)
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(resp)
|
||||||
|
}
|
16
src/api/v1/reload.go
Normal file
16
src/api/v1/reload.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
U "github.com/yusing/go-proxy/api/v1/utils"
|
||||||
|
"github.com/yusing/go-proxy/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Reload(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := cfg.Reload(); err.IsNotNil() {
|
||||||
|
U.HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
20
src/api/v1/stats.go
Normal file
20
src/api/v1/stats.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
U "github.com/yusing/go-proxy/api/v1/utils"
|
||||||
|
"github.com/yusing/go-proxy/config"
|
||||||
|
"github.com/yusing/go-proxy/server"
|
||||||
|
"github.com/yusing/go-proxy/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
|
stats := map[string]interface{}{
|
||||||
|
"proxies": cfg.Statistics(),
|
||||||
|
"uptime": utils.FormatDuration(server.GetProxyServer().Uptime()),
|
||||||
|
}
|
||||||
|
if err := U.RespondJson(stats, w); err != nil {
|
||||||
|
U.HandleErr(w, r, err)
|
||||||
|
}
|
||||||
|
}
|
32
src/api/v1/utils/error.go
Normal file
32
src/api/v1/utils/error.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleErr(w http.ResponseWriter, r *http.Request, err error, code ...int) {
|
||||||
|
err = E.From(err).Subjectf("%s %s", r.Method, r.URL)
|
||||||
|
logrus.WithField("?", "api").Error(err)
|
||||||
|
if len(code) > 0 {
|
||||||
|
http.Error(w, err.Error(), code[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrMissingKey(k string) error {
|
||||||
|
return errors.New("missing key '" + k + "' in query or request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrInvalidKey(k string) error {
|
||||||
|
return errors.New("invalid key '" + k + "' in query or request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrNotFound(k, v string) error {
|
||||||
|
return fmt.Errorf("key %q with value %q not found", k, v)
|
||||||
|
}
|
62
src/api/v1/utils/net.go
Normal file
62
src/api/v1/utils/net.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/common"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsSiteHealthy(url string) bool {
|
||||||
|
// try HEAD first
|
||||||
|
// if HEAD is not allowed, try GET
|
||||||
|
resp, err := HttpClient.Head(url)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
if err != nil && resp != nil && resp.StatusCode == http.StatusMethodNotAllowed {
|
||||||
|
_, err = HttpClient.Get(url)
|
||||||
|
}
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsStreamHealthy(scheme, address string) bool {
|
||||||
|
conn, err := net.DialTimeout(scheme, address, common.DialTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReloadServer() E.NestedError {
|
||||||
|
resp, err := HttpClient.Post(fmt.Sprintf("http://localhost%v/reload", common.APIHTTPPort), "", nil)
|
||||||
|
if err != nil {
|
||||||
|
return E.From(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return E.Failure("server reload").Subjectf("status code: %v", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
var HttpClient = &http.Client{
|
||||||
|
Timeout: common.ConnectionTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DisableKeepAlives: true,
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: common.DialTimeout,
|
||||||
|
KeepAlive: common.KeepAlive, // this is different from DisableKeepAlives
|
||||||
|
}).DialContext,
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
}
|
17
src/api/v1/utils/utils.go
Normal file
17
src/api/v1/utils/utils.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RespondJson(data any, w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
j, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
w.Write(j)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
78
src/autocert/config.go
Normal file
78
src/autocert/config.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
package autocert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/certcrypto"
|
||||||
|
"github.com/go-acme/lego/v4/lego"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
M "github.com/yusing/go-proxy/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config M.AutoCertConfig
|
||||||
|
|
||||||
|
func NewConfig(cfg *M.AutoCertConfig) *Config {
|
||||||
|
if cfg.CertPath == "" {
|
||||||
|
cfg.CertPath = CertFileDefault
|
||||||
|
}
|
||||||
|
if cfg.KeyPath == "" {
|
||||||
|
cfg.KeyPath = KeyFileDefault
|
||||||
|
}
|
||||||
|
return (*Config)(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) GetProvider() (*Provider, E.NestedError) {
|
||||||
|
errors := E.NewBuilder("cannot create autocert provider")
|
||||||
|
|
||||||
|
if cfg.Provider != ProviderLocal {
|
||||||
|
if len(cfg.Domains) == 0 {
|
||||||
|
errors.Addf("no domains specified")
|
||||||
|
}
|
||||||
|
if cfg.Provider == "" {
|
||||||
|
errors.Addf("no provider specified")
|
||||||
|
}
|
||||||
|
if cfg.Email == "" {
|
||||||
|
errors.Addf("no email specified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gen, ok := providersGenMap[cfg.Provider]
|
||||||
|
if !ok {
|
||||||
|
errors.Addf("unknown provider: %q", cfg.Provider)
|
||||||
|
}
|
||||||
|
if err := errors.Build(); err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
privKey, err := E.Check(ecdsa.GenerateKey(elliptic.P256(), rand.Reader))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, E.Failure("generate private key").With(err)
|
||||||
|
}
|
||||||
|
user := &User{
|
||||||
|
Email: cfg.Email,
|
||||||
|
key: privKey,
|
||||||
|
}
|
||||||
|
legoCfg := lego.NewConfig(user)
|
||||||
|
legoCfg.Certificate.KeyType = certcrypto.RSA2048
|
||||||
|
legoClient, err := E.Check(lego.NewClient(legoCfg))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, E.Failure("create lego client").With(err)
|
||||||
|
}
|
||||||
|
base := &Provider{
|
||||||
|
cfg: cfg,
|
||||||
|
user: user,
|
||||||
|
legoCfg: legoCfg,
|
||||||
|
client: legoClient,
|
||||||
|
}
|
||||||
|
legoProvider, err := E.Check(gen(cfg.Options))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, E.Failure("create lego provider").With(err)
|
||||||
|
}
|
||||||
|
err = E.From(legoClient.Challenge.SetDNS01Provider(legoProvider))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, E.Failure("set challenge provider").With(err)
|
||||||
|
}
|
||||||
|
return base, E.Nil()
|
||||||
|
}
|
30
src/autocert/constants.go
Normal file
30
src/autocert/constants.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package autocert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
certBasePath = "certs/"
|
||||||
|
CertFileDefault = certBasePath + "cert.crt"
|
||||||
|
KeyFileDefault = certBasePath + "priv.key"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProviderLocal = "local"
|
||||||
|
ProviderCloudflare = "cloudflare"
|
||||||
|
ProviderClouddns = "clouddns"
|
||||||
|
ProviderDuckdns = "duckdns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var providersGenMap = map[string]ProviderGenerator{
|
||||||
|
ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
||||||
|
ProviderCloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
||||||
|
ProviderClouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
||||||
|
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
||||||
|
}
|
||||||
|
|
||||||
|
var Logger = logrus.WithField("?", "autocert")
|
20
src/autocert/dummy.go
Normal file
20
src/autocert/dummy.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package autocert
|
||||||
|
|
||||||
|
type DummyConfig struct{}
|
||||||
|
type DummyProvider struct{}
|
||||||
|
|
||||||
|
func NewDummyDefaultConfig() *DummyConfig {
|
||||||
|
return &DummyConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDummyDNSProviderConfig(*DummyConfig) (*DummyProvider, error) {
|
||||||
|
return &DummyProvider{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DummyProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DummyProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
return nil
|
||||||
|
}
|
258
src/autocert/provider.go
Normal file
258
src/autocert/provider.go
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
package autocert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/certificate"
|
||||||
|
"github.com/go-acme/lego/v4/challenge"
|
||||||
|
"github.com/go-acme/lego/v4/lego"
|
||||||
|
"github.com/go-acme/lego/v4/registration"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
M "github.com/yusing/go-proxy/models"
|
||||||
|
"github.com/yusing/go-proxy/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
cfg *Config
|
||||||
|
user *User
|
||||||
|
legoCfg *lego.Config
|
||||||
|
client *lego.Client
|
||||||
|
|
||||||
|
tlsCert *tls.Certificate
|
||||||
|
certExpiries CertExpiries
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderGenerator func(M.AutocertProviderOpt) (challenge.Provider, error)
|
||||||
|
type CertExpiries map[string]time.Time
|
||||||
|
|
||||||
|
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
if p.tlsCert == nil {
|
||||||
|
return nil, E.Failure("get certificate")
|
||||||
|
}
|
||||||
|
return p.tlsCert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) GetName() string {
|
||||||
|
return p.cfg.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) GetCertPath() string {
|
||||||
|
return p.cfg.CertPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) GetKeyPath() string {
|
||||||
|
return p.cfg.KeyPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) GetExpiries() CertExpiries {
|
||||||
|
return p.certExpiries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) ObtainCert() E.NestedError {
|
||||||
|
ne := E.Failure("obtain certificate")
|
||||||
|
|
||||||
|
client := p.client
|
||||||
|
if p.user.Registration == nil {
|
||||||
|
reg, err := E.Check(client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return ne.With(E.Failure("register account").With(err))
|
||||||
|
}
|
||||||
|
p.user.Registration = reg
|
||||||
|
}
|
||||||
|
req := certificate.ObtainRequest{
|
||||||
|
Domains: p.cfg.Domains,
|
||||||
|
Bundle: true,
|
||||||
|
}
|
||||||
|
cert, err := E.Check(client.Certificate.Obtain(req))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return ne.With(err)
|
||||||
|
}
|
||||||
|
err = p.saveCert(cert)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return ne.With(E.Failure("save certificate").With(err))
|
||||||
|
}
|
||||||
|
tlsCert, err := E.Check(tls.X509KeyPair(cert.Certificate, cert.PrivateKey))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return ne.With(E.Failure("parse obtained certificate").With(err))
|
||||||
|
}
|
||||||
|
expiries, err := getCertExpiries(&tlsCert)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return ne.With(E.Failure("get certificate expiry").With(err))
|
||||||
|
}
|
||||||
|
p.tlsCert = &tlsCert
|
||||||
|
p.certExpiries = expiries
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) LoadCert() E.NestedError {
|
||||||
|
cert, err := E.Check(tls.LoadX509KeyPair(p.cfg.CertPath, p.cfg.KeyPath))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
expiries, err := getCertExpiries(&cert)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.tlsCert = &cert
|
||||||
|
p.certExpiries = expiries
|
||||||
|
p.renewIfNeeded()
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) ShouldRenewOn() time.Time {
|
||||||
|
for _, expiry := range p.certExpiries {
|
||||||
|
return expiry.AddDate(0, -1, 0)
|
||||||
|
}
|
||||||
|
// this line should never be reached
|
||||||
|
panic("no certificate available")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) ScheduleRenewal(ctx context.Context) {
|
||||||
|
if p.GetName() == ProviderLocal {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("starting renewal scheduler")
|
||||||
|
defer logger.Debug("renewal scheduler stopped")
|
||||||
|
|
||||||
|
stop := make(chan struct{})
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
t := time.Until(p.ShouldRenewOn())
|
||||||
|
Logger.Infof("next renewal in %v", t.Round(time.Second))
|
||||||
|
go func() {
|
||||||
|
<-time.After(t)
|
||||||
|
close(stop)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-stop:
|
||||||
|
if err := p.renewIfNeeded(); err.IsNotNil() {
|
||||||
|
Logger.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) saveCert(cert *certificate.Resource) E.NestedError {
|
||||||
|
err := os.WriteFile(p.cfg.KeyPath, cert.PrivateKey, 0600) // -rw-------
|
||||||
|
if err != nil {
|
||||||
|
return E.Failure("write key file").With(err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(p.cfg.CertPath, cert.Certificate, 0644) // -rw-r--r--
|
||||||
|
if err != nil {
|
||||||
|
return E.Failure("write cert file").With(err)
|
||||||
|
}
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) needRenewal() bool {
|
||||||
|
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 *Provider) renewIfNeeded() E.NestedError {
|
||||||
|
if !p.needRenewal() {
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mutex.Lock()
|
||||||
|
defer p.mutex.Unlock()
|
||||||
|
|
||||||
|
if !p.needRenewal() {
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
trials := 0
|
||||||
|
for {
|
||||||
|
err := p.ObtainCert()
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
trials++
|
||||||
|
if trials > 3 {
|
||||||
|
return E.Failure("renew certificate").With(err)
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCertExpiries(cert *tls.Certificate) (CertExpiries, E.NestedError) {
|
||||||
|
r := make(CertExpiries, len(cert.Certificate))
|
||||||
|
for _, cert := range cert.Certificate {
|
||||||
|
x509Cert, err := E.Check(x509.ParseCertificate(cert))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, E.Failure("parse certificate").With(err)
|
||||||
|
}
|
||||||
|
if x509Cert.IsCA {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r[x509Cert.Subject.CommonName] = x509Cert.NotAfter
|
||||||
|
}
|
||||||
|
return r, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setOptions[T interface{}](cfg *T, opt M.AutocertProviderOpt) E.NestedError {
|
||||||
|
for k, v := range opt {
|
||||||
|
err := utils.SetFieldFromSnake(cfg, k, v)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return E.Failure("set autocert option").Subject(k).With(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func providerGenerator[CT any, PT challenge.Provider](
|
||||||
|
defaultCfg func() *CT,
|
||||||
|
newProvider func(*CT) (PT, error),
|
||||||
|
) ProviderGenerator {
|
||||||
|
return func(opt M.AutocertProviderOpt) (challenge.Provider, error) {
|
||||||
|
cfg := defaultCfg()
|
||||||
|
err := setOptions(cfg, opt)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p, err := E.Check(newProvider(cfg))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = logrus.WithField("?", "autocert")
|
22
src/autocert/user.go
Normal file
22
src/autocert/user.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package autocert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-acme/lego/v4/registration"
|
||||||
|
"crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Email string
|
||||||
|
Registration *registration.Resource
|
||||||
|
key crypto.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) GetEmail() string {
|
||||||
|
return u.Email
|
||||||
|
}
|
||||||
|
func (u *User) GetRegistration() *registration.Resource {
|
||||||
|
return u.Registration
|
||||||
|
}
|
||||||
|
func (u *User) GetPrivateKey() crypto.PrivateKey {
|
||||||
|
return u.key
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
package main
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Args struct {
|
type Args struct {
|
||||||
|
@ -18,21 +19,21 @@ const (
|
||||||
|
|
||||||
var ValidCommands = []string{CommandStart, CommandValidate, CommandReload}
|
var ValidCommands = []string{CommandStart, CommandValidate, CommandReload}
|
||||||
|
|
||||||
func getArgs() Args {
|
func GetArgs() Args {
|
||||||
var args Args
|
var args Args
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
args.Command = flag.Arg(0)
|
args.Command = flag.Arg(0)
|
||||||
if err := validateArgs(args.Command, ValidCommands); err != nil {
|
if err := validateArgs(args.Command, ValidCommands); err.IsNotNil() {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateArgs[T comparable](arg T, validArgs []T) error {
|
func validateArgs[T comparable](arg T, validArgs []T) E.NestedError {
|
||||||
for _, v := range validArgs {
|
for _, v := range validArgs {
|
||||||
if arg == v {
|
if arg == v {
|
||||||
return nil
|
return E.Nil()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return NewNestedError("invalid argument").Subjectf("%v", arg)
|
return E.Invalid("argument", arg)
|
||||||
}
|
}
|
103
src/common/constants.go
Normal file
103
src/common/constants.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConnectionTimeout = 5 * time.Second
|
||||||
|
DialTimeout = 3 * time.Second
|
||||||
|
KeepAlive = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProviderKind_Docker = "docker"
|
||||||
|
ProviderKind_File = "file"
|
||||||
|
)
|
||||||
|
|
||||||
|
// file, folder structure
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfigBasePath = "config/"
|
||||||
|
ConfigFileName = "config.yml"
|
||||||
|
ConfigPath = ConfigBasePath + ConfigFileName
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TemplatesBasePath = "templates/"
|
||||||
|
PanelTemplatePath = TemplatesBasePath + "panel/index.html"
|
||||||
|
ConfigEditorTemplatePath = TemplatesBasePath + "config_editor/index.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SchemaBasePath = "schema/"
|
||||||
|
ConfigSchemaPath = SchemaBasePath + "config.schema.json"
|
||||||
|
ProvidersSchemaPath = SchemaBasePath + "providers.schema.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DockerHostFromEnv = "FROM_ENV"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProxyHTTPPort = ":80"
|
||||||
|
ProxyHTTPSPort = ":443"
|
||||||
|
APIHTTPPort = ":8888"
|
||||||
|
PanelHTTPPort = ":8080"
|
||||||
|
)
|
||||||
|
|
||||||
|
var WellKnownHTTPPorts = map[uint16]bool{
|
||||||
|
80: true,
|
||||||
|
8000: true,
|
||||||
|
8008: true,
|
||||||
|
8080: true,
|
||||||
|
3000: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ImageNamePortMapTCP = map[string]int{
|
||||||
|
"postgres": 5432,
|
||||||
|
"mysql": 3306,
|
||||||
|
"mariadb": 3306,
|
||||||
|
"redis": 6379,
|
||||||
|
"mssql": 1433,
|
||||||
|
"memcached": 11211,
|
||||||
|
"rabbitmq": 5672,
|
||||||
|
"mongo": 27017,
|
||||||
|
}
|
||||||
|
ExtraNamePortMapTCP = map[string]int{
|
||||||
|
"dns": 53,
|
||||||
|
"ssh": 22,
|
||||||
|
"ftp": 21,
|
||||||
|
"smtp": 25,
|
||||||
|
"pop3": 110,
|
||||||
|
"imap": 143,
|
||||||
|
}
|
||||||
|
NamePortMapTCP = func() map[string]int {
|
||||||
|
m := make(map[string]int)
|
||||||
|
for k, v := range ImageNamePortMapTCP {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range ExtraNamePortMapTCP {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}()
|
||||||
|
)
|
||||||
|
|
||||||
|
// docker library uses uint16, so followed here
|
||||||
|
var ImageNamePortMapHTTP = map[string]uint16{
|
||||||
|
"nginx": 80,
|
||||||
|
"httpd": 80,
|
||||||
|
"adguardhome": 3000,
|
||||||
|
"gogs": 3000,
|
||||||
|
"gitea": 3000,
|
||||||
|
"portainer": 9000,
|
||||||
|
"portainer-ce": 9000,
|
||||||
|
"home-assistant": 8123,
|
||||||
|
"homebridge": 8581,
|
||||||
|
"uptime-kuma": 3001,
|
||||||
|
"changedetection.io": 3000,
|
||||||
|
"prometheus": 9090,
|
||||||
|
"grafana": 3000,
|
||||||
|
"dockge": 5001,
|
||||||
|
"nginx-proxy-manager": 81,
|
||||||
|
}
|
24
src/common/env.go
Normal file
24
src/common/env.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var IsRunningAsService = getEnvBool("GOPROXY_IS_SYSTEMD")
|
||||||
|
var NoSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION")
|
||||||
|
var IsDebug = getEnvBool("GOPROXY_DEBUG")
|
||||||
|
|
||||||
|
var LogLevel = func() logrus.Level {
|
||||||
|
if IsDebug {
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
}
|
||||||
|
return logrus.GetLevel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
func getEnvBool(key string) bool {
|
||||||
|
v := os.Getenv(key)
|
||||||
|
return v == "1" || strings.ToLower(v) == "true" || strings.ToLower(v) == "yes" || strings.ToLower(v) == "on"
|
||||||
|
}
|
262
src/config/config.go
Normal file
262
src/config/config.go
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/yusing/go-proxy/autocert"
|
||||||
|
"github.com/yusing/go-proxy/common"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
M "github.com/yusing/go-proxy/models"
|
||||||
|
PR "github.com/yusing/go-proxy/proxy/provider"
|
||||||
|
R "github.com/yusing/go-proxy/route"
|
||||||
|
U "github.com/yusing/go-proxy/utils"
|
||||||
|
F "github.com/yusing/go-proxy/utils/functional"
|
||||||
|
W "github.com/yusing/go-proxy/watcher"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
value *M.Config
|
||||||
|
|
||||||
|
l logrus.FieldLogger
|
||||||
|
reader U.Reader
|
||||||
|
proxyProviders *F.Map[string, *PR.Provider]
|
||||||
|
autocertProvider *autocert.Provider
|
||||||
|
|
||||||
|
watcher W.Watcher
|
||||||
|
watcherCtx context.Context
|
||||||
|
watcherCancel context.CancelFunc
|
||||||
|
reloadReq chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() (*Config, E.NestedError) {
|
||||||
|
cfg := &Config{
|
||||||
|
l: logrus.WithField("?", "config"),
|
||||||
|
reader: U.NewFileReader(common.ConfigPath),
|
||||||
|
watcher: W.NewFileWatcher(common.ConfigFileName),
|
||||||
|
reloadReq: make(chan struct{}),
|
||||||
|
}
|
||||||
|
if err := cfg.load(); err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.watchChanges()
|
||||||
|
return cfg, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Validate(data []byte) E.NestedError {
|
||||||
|
return U.ValidateYaml(U.GetSchema(common.ConfigSchemaPath), data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) Value() M.Config {
|
||||||
|
return *cfg.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) GetAutoCertProvider() *autocert.Provider {
|
||||||
|
return cfg.autocertProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) Dispose() {
|
||||||
|
cfg.watcherCancel()
|
||||||
|
cfg.l.Debug("stopped watcher")
|
||||||
|
cfg.stopProviders()
|
||||||
|
cfg.l.Debug("stopped providers")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) Reload() E.NestedError {
|
||||||
|
cfg.stopProviders()
|
||||||
|
if err := cfg.load(); err.IsNotNil() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg.startProviders()
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) FindRoute(alias string) R.Route {
|
||||||
|
r := cfg.proxyProviders.Find(
|
||||||
|
func(p *PR.Provider) (any, bool) {
|
||||||
|
rs := p.GetCurrentRoutes()
|
||||||
|
if rs.Contains(alias) {
|
||||||
|
return rs.Get(alias), true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.(R.Route)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject {
|
||||||
|
routes := make(map[string]U.SerializedObject)
|
||||||
|
cfg.proxyProviders.Each(func(p *PR.Provider) {
|
||||||
|
prName := p.GetName()
|
||||||
|
p.GetCurrentRoutes().EachKV(func(a string, r R.Route) {
|
||||||
|
obj, err := U.Serialize(r)
|
||||||
|
if err != nil {
|
||||||
|
cfg.l.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
obj["provider"] = prName
|
||||||
|
switch r.(type) {
|
||||||
|
case *R.StreamRoute:
|
||||||
|
obj["type"] = "stream"
|
||||||
|
case *R.HTTPRoute:
|
||||||
|
obj["type"] = "reverse_proxy"
|
||||||
|
default:
|
||||||
|
panic("bug: should not reach here")
|
||||||
|
}
|
||||||
|
routes[a] = obj
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return routes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) Statistics() map[string]interface{} {
|
||||||
|
nTotalStreams := 0
|
||||||
|
nTotalRPs := 0
|
||||||
|
providerStats := make(map[string]interface{})
|
||||||
|
|
||||||
|
cfg.proxyProviders.Each(func(p *PR.Provider) {
|
||||||
|
stats := make(map[string]interface{})
|
||||||
|
nStreams := 0
|
||||||
|
nRPs := 0
|
||||||
|
p.GetCurrentRoutes().EachKV(func(a string, r R.Route) {
|
||||||
|
switch r.(type) {
|
||||||
|
case *R.StreamRoute:
|
||||||
|
nStreams++
|
||||||
|
nTotalStreams++
|
||||||
|
case *R.HTTPRoute:
|
||||||
|
nRPs++
|
||||||
|
nTotalRPs++
|
||||||
|
default:
|
||||||
|
panic("bug: should not reach here")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
stats["num_streams"] = nStreams
|
||||||
|
stats["num_reverse_proxies"] = nRPs
|
||||||
|
switch p.ProviderImpl.(type) {
|
||||||
|
case *PR.DockerProvider:
|
||||||
|
stats["type"] = "docker"
|
||||||
|
case *PR.FileProvider:
|
||||||
|
stats["type"] = "file"
|
||||||
|
default:
|
||||||
|
panic("bug: should not reach here")
|
||||||
|
}
|
||||||
|
providerStats[p.GetName()] = stats
|
||||||
|
})
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"num_total_streams": nTotalStreams,
|
||||||
|
"num_total_reverse_proxies": nTotalRPs,
|
||||||
|
"providers": providerStats,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) watchChanges() {
|
||||||
|
cfg.watcherCtx, cfg.watcherCancel = context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cfg.watcherCtx.Done():
|
||||||
|
return
|
||||||
|
case <-cfg.reloadReq:
|
||||||
|
if err := cfg.Reload(); err.IsNotNil() {
|
||||||
|
cfg.l.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
eventCh, errCh := cfg.watcher.Events(cfg.watcherCtx)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cfg.watcherCtx.Done():
|
||||||
|
return
|
||||||
|
case event := <-eventCh:
|
||||||
|
if event.Action.IsDelete() {
|
||||||
|
cfg.stopProviders()
|
||||||
|
} else {
|
||||||
|
cfg.reloadReq <- struct{}{}
|
||||||
|
}
|
||||||
|
case err := <-errCh:
|
||||||
|
cfg.l.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) load() E.NestedError {
|
||||||
|
cfg.l.Debug("loading config")
|
||||||
|
|
||||||
|
data, err := cfg.reader.Read()
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return E.Failure("read config").With(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
model := M.DefaultConfig()
|
||||||
|
if err := E.From(yaml.Unmarshal(data, model)); err.IsNotNil() {
|
||||||
|
return E.Failure("parse config").With(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !common.NoSchemaValidation {
|
||||||
|
if err := Validate(data); err.IsNotNil() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errors := E.NewBuilder("errors validating config")
|
||||||
|
|
||||||
|
cfg.l.Debug("starting autocert")
|
||||||
|
ap, err := autocert.NewConfig(&model.AutoCert).GetProvider()
|
||||||
|
if err.IsNotNil() {
|
||||||
|
errors.Add(E.Failure("autocert provider").With(err))
|
||||||
|
} else {
|
||||||
|
cfg.l.Debug("started autocert")
|
||||||
|
}
|
||||||
|
cfg.autocertProvider = ap
|
||||||
|
|
||||||
|
cfg.l.Debug("starting providers")
|
||||||
|
cfg.proxyProviders = F.NewMap[string, *PR.Provider]()
|
||||||
|
for name, pm := range model.Providers {
|
||||||
|
p := PR.NewProvider(name, pm)
|
||||||
|
cfg.proxyProviders.Set(name, p)
|
||||||
|
if err := p.StartAllRoutes(); err.IsNotNil() {
|
||||||
|
errors.Add(E.Failure("start routes").Subjectf("provider %s", name).With(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.l.Debug("started providers")
|
||||||
|
|
||||||
|
cfg.value = model
|
||||||
|
|
||||||
|
if err := errors.Build(); err.IsNotNil() {
|
||||||
|
cfg.l.Warn(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.l.Debug("loaded config")
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) controlProviders(action string, do func(*PR.Provider) E.NestedError) {
|
||||||
|
errors := E.NewBuilder("cannot %s these providers", action)
|
||||||
|
|
||||||
|
cfg.proxyProviders.EachKVParallel(func(name string, p *PR.Provider) {
|
||||||
|
if err := do(p); err.IsNotNil() {
|
||||||
|
errors.Add(E.From(err).Subjectf("provider %s", name))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := errors.Build(); err.IsNotNil() {
|
||||||
|
cfg.l.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) startProviders() {
|
||||||
|
cfg.controlProviders("start", (*PR.Provider).StartAllRoutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) stopProviders() {
|
||||||
|
cfg.controlProviders("stop", (*PR.Provider).StopAllRoutes)
|
||||||
|
}
|
94
src/docker/client.go
Normal file
94
src/docker/client.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/connhelper"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/yusing/go-proxy/common"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client = *client.Client
|
||||||
|
|
||||||
|
// ConnectClient creates a new Docker client connection to the specified host.
|
||||||
|
//
|
||||||
|
// Returns existing client if available.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - host: the host to connect to (either a URL or "FROM_ENV").
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - Client: the Docker client connection.
|
||||||
|
// - error: an error if the connection failed.
|
||||||
|
func ConnectClient(host string) (Client, E.NestedError) {
|
||||||
|
clientMapMu.Lock()
|
||||||
|
defer clientMapMu.Unlock()
|
||||||
|
|
||||||
|
// check if client exists
|
||||||
|
if client, ok := clientMap[host]; ok {
|
||||||
|
return client, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// create client
|
||||||
|
var opt []client.Opt
|
||||||
|
|
||||||
|
switch host {
|
||||||
|
case common.DockerHostFromEnv:
|
||||||
|
opt = clientOptEnvHost
|
||||||
|
default:
|
||||||
|
helper, err := E.Check(connhelper.GetConnectionHelper(host))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
logger.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if helper != nil {
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: helper.Dialer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
opt = []client.Opt{
|
||||||
|
client.WithHTTPClient(httpClient),
|
||||||
|
client.WithHost(helper.Host),
|
||||||
|
client.WithAPIVersionNegotiation(),
|
||||||
|
client.WithDialContext(helper.Dialer),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opt = []client.Opt{
|
||||||
|
client.WithHost(host),
|
||||||
|
client.WithAPIVersionNegotiation(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := E.Check(client.NewClientWithOpts(opt...))
|
||||||
|
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientMap[host] = client
|
||||||
|
return client, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseAllClients() {
|
||||||
|
clientMapMu.Lock()
|
||||||
|
defer clientMapMu.Unlock()
|
||||||
|
for _, client := range clientMap {
|
||||||
|
client.Close()
|
||||||
|
}
|
||||||
|
clientMap = make(map[string]Client)
|
||||||
|
logger.Debug("closed all clients")
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientMap map[string]Client = make(map[string]Client)
|
||||||
|
var clientMapMu sync.Mutex
|
||||||
|
|
||||||
|
var clientOptEnvHost = []client.Opt{
|
||||||
|
client.WithHostFromEnv(),
|
||||||
|
client.WithAPIVersionNegotiation(),
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = logrus.WithField("?", "docker")
|
48
src/docker/client_info.go
Normal file
48
src/docker/client_info.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientInfo struct {
|
||||||
|
Host string
|
||||||
|
Containers []types.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetClientInfo(clientHost string) (*ClientInfo, E.NestedError) {
|
||||||
|
dockerClient, err := ConnectClient(clientHost)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, E.Failure("create docker client").With(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
containers, err := E.Check(dockerClient.ContainerList(ctx, container.ListOptions{All: true}))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, E.Failure("list containers").With(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract host from docker client url
|
||||||
|
// since the services being proxied to
|
||||||
|
// should have the same IP as the docker client
|
||||||
|
url, err := E.Check(client.ParseHostURL(dockerClient.DaemonHost()))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, E.Invalid("host url", dockerClient.DaemonHost()).With(err)
|
||||||
|
}
|
||||||
|
if url.Scheme == "unix" {
|
||||||
|
return &ClientInfo{Host: "localhost", Containers: containers}, E.Nil()
|
||||||
|
}
|
||||||
|
return &ClientInfo{Host: url.Hostname(), Containers: containers}, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsErrConnectionFailed(err error) bool {
|
||||||
|
return client.IsErrConnectionFailed(err)
|
||||||
|
}
|
32
src/docker/homepage_label.go
Normal file
32
src/docker/homepage_label.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
type (
|
||||||
|
HomePageConfig struct{ m map[string]HomePageCategory }
|
||||||
|
HomePageCategory []HomePageItem
|
||||||
|
|
||||||
|
HomePageItem struct {
|
||||||
|
Name string
|
||||||
|
Icon string
|
||||||
|
Category string
|
||||||
|
Description string
|
||||||
|
WidgetConfig map[string]interface{}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewHomePageConfig() *HomePageConfig {
|
||||||
|
return &HomePageConfig{m: make(map[string]HomePageCategory)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHomePageItem() *HomePageItem {
|
||||||
|
return &HomePageItem{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HomePageConfig) Clear() {
|
||||||
|
c.m = make(map[string]HomePageCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HomePageConfig) Add(item HomePageItem) {
|
||||||
|
c.m[item.Category] = HomePageCategory{item}
|
||||||
|
}
|
||||||
|
|
||||||
|
const NSHomePage = "homepage"
|
81
src/docker/label.go
Normal file
81
src/docker/label.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
U "github.com/yusing/go-proxy/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Label struct {
|
||||||
|
Namespace string
|
||||||
|
Target string
|
||||||
|
Attribute string
|
||||||
|
Value any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply applies the value of a Label to the corresponding field in the given object.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - obj: a pointer to the object to which the Label value will be applied.
|
||||||
|
// - l: a pointer to the Label containing the attribute and value to be applied.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: an error if the field does not exist.
|
||||||
|
func ApplyLabel[T any](obj *T, l *Label) E.NestedError {
|
||||||
|
return U.SetFieldFromSnake(obj, l.Attribute, l.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValueParser func(string) (any, E.NestedError)
|
||||||
|
type ValueParserMap map[string]ValueParser
|
||||||
|
|
||||||
|
func ParseLabel(label string, value string) (*Label, E.NestedError) {
|
||||||
|
parts := strings.Split(label, ".")
|
||||||
|
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return &Label{
|
||||||
|
Namespace: label,
|
||||||
|
Value: value,
|
||||||
|
}, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
l := &Label{
|
||||||
|
Namespace: parts[0],
|
||||||
|
Target: parts[1],
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) == 3 {
|
||||||
|
l.Attribute = parts[2]
|
||||||
|
} else {
|
||||||
|
l.Attribute = l.Target
|
||||||
|
}
|
||||||
|
|
||||||
|
// find if namespace has value parser
|
||||||
|
pm, ok := labelValueParserMap[l.Namespace]
|
||||||
|
if !ok {
|
||||||
|
return l, E.Nil()
|
||||||
|
}
|
||||||
|
// find if attribute has value parser
|
||||||
|
p, ok := pm[l.Attribute]
|
||||||
|
if !ok {
|
||||||
|
return l, E.Nil()
|
||||||
|
}
|
||||||
|
// try to parse value
|
||||||
|
v, err := p(value)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
l.Value = v
|
||||||
|
return l, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterNamespace(namespace string, pm ValueParserMap) {
|
||||||
|
labelValueParserMap[namespace] = pm
|
||||||
|
}
|
||||||
|
|
||||||
|
// namespace:target.attribute -> func(string) (any, error)
|
||||||
|
var labelValueParserMap = make(map[string]ValueParserMap)
|
||||||
|
|
||||||
|
var ErrInvalidLabel = errors.New("invalid label")
|
55
src/docker/proxy_label.go
Normal file
55
src/docker/proxy_label.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setHeadersParser(value string) (any, E.NestedError) {
|
||||||
|
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, E.Invalid("set header statement", line)
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
val := strings.TrimSpace(parts[1])
|
||||||
|
h.Add(key, val)
|
||||||
|
}
|
||||||
|
return h, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func commaSepParser(value string) (any, E.NestedError) {
|
||||||
|
v := strings.Split(value, ",")
|
||||||
|
for i := range v {
|
||||||
|
v[i] = strings.TrimSpace(v[i])
|
||||||
|
}
|
||||||
|
return v, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolParser(value string) (any, E.NestedError) {
|
||||||
|
switch strings.ToLower(value) {
|
||||||
|
case "true", "yes", "1":
|
||||||
|
return true, E.Nil()
|
||||||
|
case "false", "no", "0":
|
||||||
|
return false, E.Nil()
|
||||||
|
default:
|
||||||
|
return nil, E.Invalid("boolean value", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const NSProxy = "proxy"
|
||||||
|
|
||||||
|
var _ = func() int {
|
||||||
|
RegisterNamespace(NSProxy, ValueParserMap{
|
||||||
|
"aliases": commaSepParser,
|
||||||
|
"set_headers": setHeadersParser,
|
||||||
|
"hide_headers": commaSepParser,
|
||||||
|
"no_tls_verify": boolParser,
|
||||||
|
})
|
||||||
|
return 0
|
||||||
|
}()
|
207
src/docker/proxy_label_test.go
Normal file
207
src/docker/proxy_label_test.go
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeLabel(namespace string, alias string, field string) string {
|
||||||
|
return fmt.Sprintf("%s.%s.%s", namespace, alias, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidLabel(t *testing.T) {
|
||||||
|
pl, err := ParseLabel("foo.bar", "1234")
|
||||||
|
if !errors.Is(err, ErrInvalidLabel) {
|
||||||
|
t.Errorf("expected errInvalidLabel, got %s", err)
|
||||||
|
}
|
||||||
|
if pl != nil {
|
||||||
|
t.Errorf("expected nil, got %v", pl)
|
||||||
|
}
|
||||||
|
_, err = ParseLabel("proxy.foo", "bar")
|
||||||
|
if !errors.Is(err, ErrInvalidLabel) {
|
||||||
|
t.Errorf("expected errInvalidLabel, got %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHomePageLabel(t *testing.T) {
|
||||||
|
alias := "foo"
|
||||||
|
field := "ip"
|
||||||
|
v := "bar"
|
||||||
|
pl, err := ParseLabel(makeLabel(NSHomePage, alias, field), v)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
t.Errorf("expected err=nil, got %s", err)
|
||||||
|
}
|
||||||
|
if pl.Target != alias {
|
||||||
|
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||||
|
}
|
||||||
|
if pl.Attribute != field {
|
||||||
|
t.Errorf("expected field=%s, got %s", field, pl.Target)
|
||||||
|
}
|
||||||
|
if pl.Value != v {
|
||||||
|
t.Errorf("expected value=%q, got %s", v, pl.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringProxyLabel(t *testing.T) {
|
||||||
|
alias := "foo"
|
||||||
|
field := "ip"
|
||||||
|
v := "bar"
|
||||||
|
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
t.Errorf("expected err=nil, got %s", err)
|
||||||
|
}
|
||||||
|
if pl.Target != alias {
|
||||||
|
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||||
|
}
|
||||||
|
if pl.Attribute != field {
|
||||||
|
t.Errorf("expected field=%s, got %s", field, pl.Target)
|
||||||
|
}
|
||||||
|
if pl.Value != v {
|
||||||
|
t.Errorf("expected value=%q, got %s", v, pl.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolProxyLabelValid(t *testing.T) {
|
||||||
|
alias := "foo"
|
||||||
|
field := "no_tls_verify"
|
||||||
|
tests := map[string]bool{
|
||||||
|
"true": true,
|
||||||
|
"TRUE": true,
|
||||||
|
"yes": true,
|
||||||
|
"1": true,
|
||||||
|
"false": false,
|
||||||
|
"FALSE": false,
|
||||||
|
"no": false,
|
||||||
|
"0": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range tests {
|
||||||
|
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), k)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
t.Errorf("expected err=nil, got %s", err)
|
||||||
|
}
|
||||||
|
if pl.Target != alias {
|
||||||
|
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||||
|
}
|
||||||
|
if pl.Attribute != field {
|
||||||
|
t.Errorf("expected field=%s, got %s", field, pl.Attribute)
|
||||||
|
}
|
||||||
|
if pl.Value != v {
|
||||||
|
t.Errorf("expected value=%v, got %v", v, pl.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolProxyLabelInvalid(t *testing.T) {
|
||||||
|
alias := "foo"
|
||||||
|
field := "no_tls_verify"
|
||||||
|
_, err := ParseLabel(makeLabel(NSProxy, alias, field), "invalid")
|
||||||
|
if !errors.Is(err, E.ErrInvalid) {
|
||||||
|
t.Errorf("expected err InvalidProxyLabel, got %s", 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 := ParseLabel(makeLabel(NSProxy, alias, field), v)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
t.Errorf("expected err=nil, got %s", err)
|
||||||
|
}
|
||||||
|
if pl.Target != alias {
|
||||||
|
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||||
|
}
|
||||||
|
if pl.Attribute != field {
|
||||||
|
t.Errorf("expected field=%s, got %s", field, pl.Attribute)
|
||||||
|
}
|
||||||
|
hGot, ok := pl.Value.(http.Header)
|
||||||
|
if !ok {
|
||||||
|
t.Error("value is not http.Header")
|
||||||
|
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 := ParseLabel(makeLabel(NSProxy, alias, field), v)
|
||||||
|
if !errors.Is(err, E.ErrInvalid) {
|
||||||
|
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 := ParseLabel(makeLabel(NSProxy, alias, field), v)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
t.Errorf("expected err=nil, got %s", err)
|
||||||
|
}
|
||||||
|
if pl.Target != alias {
|
||||||
|
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||||
|
}
|
||||||
|
if pl.Attribute != field {
|
||||||
|
t.Errorf("expected field=%s, got %s", field, pl.Attribute)
|
||||||
|
}
|
||||||
|
sGot, ok := pl.Value.([]string)
|
||||||
|
sWant := []string{"X-Custom-Header1"}
|
||||||
|
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 := ParseLabel(makeLabel(NSProxy, alias, field), v)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
t.Errorf("expected err=nil, got %s", err)
|
||||||
|
}
|
||||||
|
if pl.Target != alias {
|
||||||
|
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||||
|
}
|
||||||
|
if pl.Attribute != field {
|
||||||
|
t.Errorf("expected field=%s, got %s", field, pl.Attribute)
|
||||||
|
}
|
||||||
|
sGot, ok := pl.Value.([]string)
|
||||||
|
sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
|
||||||
|
if !ok {
|
||||||
|
t.Error("value is not []string")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(sGot, sWant) {
|
||||||
|
t.Errorf("expected %q, got %q", sWant, sGot)
|
||||||
|
}
|
||||||
|
}
|
43
src/error/builder.go
Normal file
43
src/error/builder.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package error
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Builder struct {
|
||||||
|
message string
|
||||||
|
errors []error
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBuilder(format string, args ...any) *Builder {
|
||||||
|
return &Builder{message: fmt.Sprintf(format, args...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Add(err error) *Builder {
|
||||||
|
if err != nil {
|
||||||
|
b.Lock()
|
||||||
|
b.errors = append(b.errors, err)
|
||||||
|
b.Unlock()
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Addf(format string, args ...any) *Builder {
|
||||||
|
return b.Add(fmt.Errorf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build builds a NestedError based on the errors collected in the Builder.
|
||||||
|
//
|
||||||
|
// If there are no errors in the Builder, it returns a Nil() NestedError.
|
||||||
|
// Otherwise, it returns a NestedError with the message and the errors collected.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - NestedError: the built NestedError.
|
||||||
|
func (b *Builder) Build() NestedError {
|
||||||
|
if len(b.errors) == 0 {
|
||||||
|
return Nil()
|
||||||
|
}
|
||||||
|
return Join(b.message, b.errors...)
|
||||||
|
}
|
217
src/error/error.go
Normal file
217
src/error/error.go
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
package error
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// NestedError is an error with an inner error
|
||||||
|
// and a list of extra nested errors.
|
||||||
|
//
|
||||||
|
// It is designed to be non nil.
|
||||||
|
//
|
||||||
|
// You can use it to join multiple errors,
|
||||||
|
// or to set a inner reason for a nested error.
|
||||||
|
//
|
||||||
|
// When a method returns both valid values and errors,
|
||||||
|
// You should return (Slice/Map, NestedError).
|
||||||
|
// Caller then should handle the nested error,
|
||||||
|
// and continue with the valid values.
|
||||||
|
NestedError struct{ *nestedError }
|
||||||
|
nestedError struct {
|
||||||
|
neBase
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
neBase struct {
|
||||||
|
subject any
|
||||||
|
err error // can be nil
|
||||||
|
extras []neBase
|
||||||
|
inner *neBase // can be nil
|
||||||
|
level int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func Nil() NestedError { return NestedError{} }
|
||||||
|
|
||||||
|
func From(err error) NestedError {
|
||||||
|
if err == nil {
|
||||||
|
return Nil()
|
||||||
|
}
|
||||||
|
return NestedError{&nestedError{neBase: *copyFrom(err)}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check is a helper function that
|
||||||
|
// convert (T, error) to (T, NestedError).
|
||||||
|
func Check[T any](obj T, err error) (T, NestedError) {
|
||||||
|
return obj, From(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Join(message string, err ...error) NestedError {
|
||||||
|
extras := make([]neBase, 0, len(err))
|
||||||
|
nErr := 0
|
||||||
|
for _, e := range err {
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
extras = append(extras, *copyFrom(e))
|
||||||
|
nErr += 1
|
||||||
|
}
|
||||||
|
if nErr == 0 {
|
||||||
|
return Nil()
|
||||||
|
}
|
||||||
|
return NestedError{&nestedError{
|
||||||
|
neBase: neBase{
|
||||||
|
err: errors.New(message),
|
||||||
|
extras: extras,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFrom(err error) *neBase {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch base := err.(type) {
|
||||||
|
case *neBase:
|
||||||
|
copy := *base
|
||||||
|
return ©
|
||||||
|
}
|
||||||
|
return &neBase{err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func new(message ...string) NestedError {
|
||||||
|
if len(message) == 0 {
|
||||||
|
return From(nil)
|
||||||
|
}
|
||||||
|
return From(errors.New(strings.Join(message, " ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorf(format string, args ...any) NestedError {
|
||||||
|
return From(fmt.Errorf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne *neBase) Error() string {
|
||||||
|
var buf strings.Builder
|
||||||
|
ne.writeToSB(&buf, ne.level, "")
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne NestedError) ExtraError(err error) NestedError {
|
||||||
|
if err != nil {
|
||||||
|
ne.Lock()
|
||||||
|
ne.extras = append(ne.extras, From(err).addLevel(ne.Level()+1))
|
||||||
|
ne.Unlock()
|
||||||
|
}
|
||||||
|
return ne
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne NestedError) Extra(s string) NestedError {
|
||||||
|
return ne.ExtraError(errors.New(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne NestedError) ExtraAny(s any) NestedError {
|
||||||
|
var msg string
|
||||||
|
switch ss := s.(type) {
|
||||||
|
case error:
|
||||||
|
return ne.ExtraError(ss)
|
||||||
|
case string:
|
||||||
|
msg = ss
|
||||||
|
case fmt.Stringer:
|
||||||
|
msg = ss.String()
|
||||||
|
default:
|
||||||
|
msg = fmt.Sprint(s)
|
||||||
|
}
|
||||||
|
return ne.ExtraError(errors.New(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne NestedError) Extraf(format string, args ...any) NestedError {
|
||||||
|
return ne.ExtraError(fmt.Errorf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne NestedError) Subject(s any) NestedError {
|
||||||
|
ne.subject = s
|
||||||
|
return ne
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne NestedError) Subjectf(format string, args ...any) NestedError {
|
||||||
|
if strings.Contains(format, "%q") {
|
||||||
|
panic("Subjectf format should not contain %q")
|
||||||
|
}
|
||||||
|
if strings.Contains(format, "%w") {
|
||||||
|
panic("Subjectf format should not contain %w")
|
||||||
|
}
|
||||||
|
return ne.Subject(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne NestedError) Level() int {
|
||||||
|
return ne.level
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne *nestedError) IsNil() bool {
|
||||||
|
return ne == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne *nestedError) IsNotNil() bool {
|
||||||
|
return ne != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne NestedError) With(inner error) NestedError {
|
||||||
|
ne.Lock()
|
||||||
|
defer ne.Unlock()
|
||||||
|
|
||||||
|
if ne.inner == nil {
|
||||||
|
ne.inner = copyFrom(inner)
|
||||||
|
} else {
|
||||||
|
ne.ExtraError(inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
root := &ne.neBase
|
||||||
|
for root.inner != nil {
|
||||||
|
root.inner.level = root.level + 1
|
||||||
|
root = root.inner
|
||||||
|
}
|
||||||
|
return ne
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne *neBase) addLevel(level int) neBase {
|
||||||
|
ret := *ne
|
||||||
|
ret.level += level
|
||||||
|
if ret.inner != nil {
|
||||||
|
inner := ret.inner.addLevel(level)
|
||||||
|
ret.inner = &inner
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne *neBase) writeToSB(sb *strings.Builder, level int, prefix string) {
|
||||||
|
ne.writeIndents(sb, level)
|
||||||
|
sb.WriteString(prefix)
|
||||||
|
|
||||||
|
if ne.err != nil {
|
||||||
|
sb.WriteString(ne.err.Error())
|
||||||
|
sb.WriteRune(' ')
|
||||||
|
}
|
||||||
|
if ne.subject != nil {
|
||||||
|
sb.WriteString(fmt.Sprintf("for %q", ne.subject))
|
||||||
|
}
|
||||||
|
if ne.inner != nil || len(ne.extras) > 0 {
|
||||||
|
sb.WriteString(":\n")
|
||||||
|
}
|
||||||
|
level += 1
|
||||||
|
for _, extra := range ne.extras {
|
||||||
|
extra.writeToSB(sb, level, "- ")
|
||||||
|
sb.WriteRune('\n')
|
||||||
|
}
|
||||||
|
if ne.inner != nil {
|
||||||
|
ne.inner.writeToSB(sb, level, "- ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ne *neBase) writeIndents(sb *strings.Builder, level int) {
|
||||||
|
for i := 0; i < level; i++ {
|
||||||
|
sb.WriteString(" ")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package error
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -12,35 +12,35 @@ func AssertEq(t *testing.T, got, want string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorSimple(t *testing.T) {
|
func TestErrorSimple(t *testing.T) {
|
||||||
ne := NewNestedError("foo bar")
|
ne := new("foo bar")
|
||||||
AssertEq(t, ne.Error(), "foo bar")
|
AssertEq(t, ne.Error(), "foo bar")
|
||||||
ne.Subject("baz")
|
ne.Subject("baz")
|
||||||
AssertEq(t, ne.Error(), "baz: foo bar")
|
AssertEq(t, ne.Error(), "baz: foo bar")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorSubjectOnly(t *testing.T) {
|
func TestErrorSubjectOnly(t *testing.T) {
|
||||||
ne := NewNestedError("").Subject("bar")
|
ne := new().Subject("bar")
|
||||||
AssertEq(t, ne.Error(), "bar")
|
AssertEq(t, ne.Error(), "bar")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorExtra(t *testing.T) {
|
func TestErrorExtra(t *testing.T) {
|
||||||
ne := NewNestedError("foo").Extra("bar").Extra("baz")
|
ne := new("foo").Extra("bar").Extra("baz")
|
||||||
AssertEq(t, ne.Error(), "foo:\n - bar\n - baz\n")
|
AssertEq(t, ne.Error(), "foo:\n - bar\n - baz\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorNested(t *testing.T) {
|
func TestErrorNested(t *testing.T) {
|
||||||
inner := NewNestedError("inner").
|
inner := new("inner").
|
||||||
Extra("123").
|
Extra("123").
|
||||||
Extra("456")
|
Extra("456")
|
||||||
inner2 := NewNestedError("inner").
|
inner2 := new("inner").
|
||||||
Subject("2").
|
Subject("2").
|
||||||
Extra("456").
|
Extra("456").
|
||||||
Extra("789")
|
Extra("789")
|
||||||
inner3 := NewNestedError("inner").
|
inner3 := new("inner").
|
||||||
Subject("3").
|
Subject("3").
|
||||||
Extra("456").
|
Extra("456").
|
||||||
Extra("789")
|
Extra("789")
|
||||||
ne := NewNestedError("foo").
|
ne := new("foo").
|
||||||
Extra("bar").
|
Extra("bar").
|
||||||
Extra("baz").
|
Extra("baz").
|
||||||
ExtraError(inner).
|
ExtraError(inner).
|
30
src/error/errors.go
Normal file
30
src/error/errors.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package error
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAlreadyStarted = new("already started")
|
||||||
|
ErrNotStarted = new("not started")
|
||||||
|
ErrInvalid = new("invalid")
|
||||||
|
ErrUnsupported = new("unsupported")
|
||||||
|
ErrNotExists = new("does not exist")
|
||||||
|
ErrDuplicated = new("duplicated")
|
||||||
|
)
|
||||||
|
|
||||||
|
func Failure(what string) NestedError {
|
||||||
|
return errorf("%s failed", what)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Invalid(subject, what any) NestedError {
|
||||||
|
return errorf("%w %s: %q", ErrInvalid, subject, what)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unsupported(subject, what any) NestedError {
|
||||||
|
return errorf("%w %s: %q", ErrUnsupported, subject, what)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotExists(subject, what any) NestedError {
|
||||||
|
return errorf("%s %w: %q", subject, ErrNotExists, what)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Duplicated(subject, what any) NestedError {
|
||||||
|
return errorf("%w %s: %q", ErrDuplicated, subject, what)
|
||||||
|
}
|
|
@ -1,331 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"slices"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/certcrypto"
|
|
||||||
"github.com/go-acme/lego/v4/certificate"
|
|
||||||
"github.com/go-acme/lego/v4/challenge"
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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"`
|
|
||||||
Domains []string `yaml:",flow" json:"domains"`
|
|
||||||
Provider string `json:"provider"`
|
|
||||||
Options ProviderOptions `yaml:",flow" json:"options"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AutoCertUser struct {
|
|
||||||
Email string
|
|
||||||
Registration *registration.Resource
|
|
||||||
key crypto.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *AutoCertUser) GetEmail() string {
|
|
||||||
return u.Email
|
|
||||||
}
|
|
||||||
func (u *AutoCertUser) GetRegistration() *registration.Resource {
|
|
||||||
return u.Registration
|
|
||||||
}
|
|
||||||
func (u *AutoCertUser) GetPrivateKey() crypto.PrivateKey {
|
|
||||||
return u.key
|
|
||||||
}
|
|
||||||
|
|
||||||
type AutoCertProvider interface {
|
|
||||||
GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error)
|
|
||||||
GetName() string
|
|
||||||
GetExpiries() CertExpiries
|
|
||||||
LoadCert() bool
|
|
||||||
ObtainCert() NestedErrorLike
|
|
||||||
ShouldRenewOn() time.Time
|
|
||||||
ScheduleRenewal()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg AutoCertConfig) GetProvider() (AutoCertProvider, error) {
|
|
||||||
ne := NewNestedError("invalid autocert config")
|
|
||||||
|
|
||||||
if len(cfg.Domains) == 0 {
|
|
||||||
ne.Extra("no domains specified")
|
|
||||||
}
|
|
||||||
if cfg.Provider == "" {
|
|
||||||
ne.Extra("no provider specified")
|
|
||||||
}
|
|
||||||
if cfg.Email == "" {
|
|
||||||
ne.Extra("no email specified")
|
|
||||||
}
|
|
||||||
gen, ok := providersGenMap[cfg.Provider]
|
|
||||||
if !ok {
|
|
||||||
ne.Extraf("unknown provider: %q", cfg.Provider)
|
|
||||||
}
|
|
||||||
if ne.HasExtras() {
|
|
||||||
return nil, ne
|
|
||||||
}
|
|
||||||
|
|
||||||
ne = NewNestedError("unable to create provider")
|
|
||||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ne.With(NewNestedError("unable to generate private key").With(err))
|
|
||||||
}
|
|
||||||
user := &AutoCertUser{
|
|
||||||
Email: cfg.Email,
|
|
||||||
key: privKey,
|
|
||||||
}
|
|
||||||
legoCfg := lego.NewConfig(user)
|
|
||||||
legoCfg.Certificate.KeyType = certcrypto.RSA2048
|
|
||||||
legoClient, err := lego.NewClient(legoCfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ne.With(NewNestedError("unable to create lego client").With(err))
|
|
||||||
}
|
|
||||||
base := &autoCertProvider{
|
|
||||||
name: cfg.Provider,
|
|
||||||
cfg: cfg,
|
|
||||||
user: user,
|
|
||||||
legoCfg: legoCfg,
|
|
||||||
client: legoClient,
|
|
||||||
}
|
|
||||||
legoProvider, err := gen(cfg.Options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ne.With(err)
|
|
||||||
}
|
|
||||||
err = legoClient.Challenge.SetDNS01Provider(legoProvider)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ne.With(NewNestedError("unable to set challenge provider").With(err))
|
|
||||||
}
|
|
||||||
return base, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type autoCertProvider struct {
|
|
||||||
name string
|
|
||||||
cfg AutoCertConfig
|
|
||||||
user *AutoCertUser
|
|
||||||
legoCfg *lego.Config
|
|
||||||
client *lego.Client
|
|
||||||
|
|
||||||
tlsCert *tls.Certificate
|
|
||||||
certExpiries CertExpiries
|
|
||||||
mutex sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *autoCertProvider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
if p.tlsCert == nil {
|
|
||||||
return nil, NewNestedError("no certificate available")
|
|
||||||
}
|
|
||||||
return p.tlsCert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *autoCertProvider) GetName() string {
|
|
||||||
return p.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *autoCertProvider) GetExpiries() CertExpiries {
|
|
||||||
return p.certExpiries
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *autoCertProvider) ObtainCert() NestedErrorLike {
|
|
||||||
ne := NewNestedError("failed to obtain certificate")
|
|
||||||
|
|
||||||
client := p.client
|
|
||||||
if p.user.Registration == nil {
|
|
||||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
|
||||||
if err != nil {
|
|
||||||
return ne.With(NewNestedError("failed to register account").With(err))
|
|
||||||
}
|
|
||||||
p.user.Registration = reg
|
|
||||||
}
|
|
||||||
req := certificate.ObtainRequest{
|
|
||||||
Domains: p.cfg.Domains,
|
|
||||||
Bundle: true,
|
|
||||||
}
|
|
||||||
cert, err := client.Certificate.Obtain(req)
|
|
||||||
if err != nil {
|
|
||||||
return ne.With(err)
|
|
||||||
}
|
|
||||||
err = p.saveCert(cert)
|
|
||||||
if err != nil {
|
|
||||||
return ne.With(NewNestedError("failed to save certificate").With(err))
|
|
||||||
}
|
|
||||||
tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return ne.With(NewNestedError("failed to parse obtained certificate").With(err))
|
|
||||||
}
|
|
||||||
expiries, err := getCertExpiries(&tlsCert)
|
|
||||||
if err != nil {
|
|
||||||
return ne.With(NewNestedError("failed to get certificate expiry").With(err))
|
|
||||||
}
|
|
||||||
p.tlsCert = &tlsCert
|
|
||||||
p.certExpiries = expiries
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *autoCertProvider) LoadCert() bool {
|
|
||||||
cert, err := tls.LoadX509KeyPair(certFileDefault, keyFileDefault)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
expiries, err := getCertExpiries(&cert)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
p.tlsCert = &cert
|
|
||||||
p.certExpiries = expiries
|
|
||||||
p.renewIfNeeded()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *autoCertProvider) ShouldRenewOn() time.Time {
|
|
||||||
for _, expiry := range p.certExpiries {
|
|
||||||
return expiry.AddDate(0, -1, 0)
|
|
||||||
}
|
|
||||||
// this line should never be reached
|
|
||||||
panic("no certificate available")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *autoCertProvider) ScheduleRenewal() {
|
|
||||||
for {
|
|
||||||
t := time.Until(p.ShouldRenewOn())
|
|
||||||
aclog.Infof("next renewal in %v", t.Round(time.Second))
|
|
||||||
time.Sleep(t)
|
|
||||||
err := p.renewIfNeeded()
|
|
||||||
if err != nil {
|
|
||||||
aclog.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *autoCertProvider) saveCert(cert *certificate.Resource) NestedErrorLike {
|
|
||||||
err := os.MkdirAll(path.Dir(certFileDefault), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return NewNestedError("unable to create cert directory").With(err)
|
|
||||||
}
|
|
||||||
err = os.WriteFile(keyFileDefault, cert.PrivateKey, 0600) // -rw-------
|
|
||||||
if err != nil {
|
|
||||||
return NewNestedError("unable to write key file").With(err)
|
|
||||||
}
|
|
||||||
err = os.WriteFile(certFileDefault, cert.Certificate, 0644) // -rw-r--r--
|
|
||||||
if err != nil {
|
|
||||||
return NewNestedError("unable to write cert file").With(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *autoCertProvider) needRenewal() bool {
|
|
||||||
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 {
|
|
||||||
if !p.needRenewal() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.mutex.Lock()
|
|
||||||
defer p.mutex.Unlock()
|
|
||||||
|
|
||||||
if !p.needRenewal() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
trials := 0
|
|
||||||
for {
|
|
||||||
err := p.ObtainCert()
|
|
||||||
if err == nil {
|
|
||||||
aclog.Info("renewed certificate")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
trials++
|
|
||||||
if trials > 3 {
|
|
||||||
return NewNestedError("failed to renew certificate after 3 trials").With(err)
|
|
||||||
}
|
|
||||||
aclog.Errorf("failed to renew certificate: %v, trying again in 5 seconds", err)
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func providerGenerator[CT any, PT challenge.Provider](
|
|
||||||
defaultCfg func() *CT,
|
|
||||||
newProvider func(*CT) (PT, error),
|
|
||||||
) ProviderGenerator {
|
|
||||||
return func(opt ProviderOptions) (challenge.Provider, error) {
|
|
||||||
cfg := defaultCfg()
|
|
||||||
err := setOptions(cfg, opt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p, err := newProvider(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCertExpiries(cert *tls.Certificate) (CertExpiries, error) {
|
|
||||||
r := make(CertExpiries, len(cert.Certificate))
|
|
||||||
for _, cert := range cert.Certificate {
|
|
||||||
x509Cert, err := x509.ParseCertificate(cert)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewNestedError("unable to parse certificate").With(err)
|
|
||||||
}
|
|
||||||
if x509Cert.IsCA {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r[x509Cert.Subject.CommonName] = x509Cert.NotAfter
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setOptions[T interface{}](cfg *T, opt ProviderOptions) error {
|
|
||||||
for k, v := range opt {
|
|
||||||
err := setFieldFromSnake(cfg, k, v)
|
|
||||||
if err != nil {
|
|
||||||
return NewNestedError("unable to set option").Subject(k).With(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var providersGenMap = map[string]ProviderGenerator{
|
|
||||||
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
|
||||||
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
|
||||||
"duckdns": providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
|
||||||
}
|
|
|
@ -1,200 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// commented out if unused
|
|
||||||
type Config interface {
|
|
||||||
Value() configModel
|
|
||||||
// Load() error
|
|
||||||
MustLoad()
|
|
||||||
GetAutoCertProvider() (AutoCertProvider, error)
|
|
||||||
// MustReload()
|
|
||||||
Reload() error
|
|
||||||
StartProviders()
|
|
||||||
StopProviders()
|
|
||||||
WatchChanges()
|
|
||||||
StopWatching()
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConfig(path string) Config {
|
|
||||||
cfg := &config{
|
|
||||||
reader: &FileReader{Path: path},
|
|
||||||
l: cfgl,
|
|
||||||
}
|
|
||||||
// must init fields above before creating watcher
|
|
||||||
cfg.watcher = cfg.NewFileWatcher()
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateConfig(data []byte) error {
|
|
||||||
cfg := &config{reader: &ByteReader{data}}
|
|
||||||
return cfg.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *config) Value() configModel {
|
|
||||||
return *cfg.m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *config) Load() error {
|
|
||||||
if cfg.reader == nil {
|
|
||||||
panic("config reader not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := cfg.reader.Read()
|
|
||||||
if err != nil {
|
|
||||||
return NewNestedError("unable to read config file").With(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
model := defaultConfig()
|
|
||||||
if err := yaml.Unmarshal(data, model); err != nil {
|
|
||||||
return NewNestedError("unable to parse config file").With(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ne := NewNestedError("invalid config")
|
|
||||||
|
|
||||||
err = validateYaml(configSchema, data)
|
|
||||||
if err != nil {
|
|
||||||
ne.With(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pErrs := NewNestedError("these providers have errors")
|
|
||||||
|
|
||||||
for name, p := range model.Providers {
|
|
||||||
if p.Kind != ProviderKind_File {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, err := p.ValidateFile()
|
|
||||||
if err != nil {
|
|
||||||
pErrs.ExtraError(
|
|
||||||
NewNestedError("provider file validation error").
|
|
||||||
Subject(name).
|
|
||||||
With(err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pErrs.HasExtras() {
|
|
||||||
ne.With(pErrs)
|
|
||||||
}
|
|
||||||
if ne.HasInner() {
|
|
||||||
return ne
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.mutex.Lock()
|
|
||||||
defer cfg.mutex.Unlock()
|
|
||||||
|
|
||||||
cfg.m = model
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *config) MustLoad() {
|
|
||||||
if err := cfg.Load(); err != nil {
|
|
||||||
cfg.l.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *config) GetAutoCertProvider() (AutoCertProvider, error) {
|
|
||||||
return cfg.m.AutoCert.GetProvider()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *config) Reload() error {
|
|
||||||
cfg.StopProviders()
|
|
||||||
if err := cfg.Load(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cfg.StartProviders()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *config) MustReload() {
|
|
||||||
if err := cfg.Reload(); err != nil {
|
|
||||||
cfg.l.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *config) StartProviders() {
|
|
||||||
if cfg.providerInitialized {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.mutex.Lock()
|
|
||||||
defer cfg.mutex.Unlock()
|
|
||||||
if cfg.providerInitialized {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pErrs := NewNestedError("failed to start these providers")
|
|
||||||
|
|
||||||
ParallelForEachKeyValue(cfg.m.Providers, func(name string, p *Provider) {
|
|
||||||
err := p.Init(name)
|
|
||||||
if err != nil {
|
|
||||||
pErrs.ExtraError(NewNestedErrorFrom(err).Subjectf("%s providers %q", p.Kind, name))
|
|
||||||
delete(cfg.m.Providers, name)
|
|
||||||
}
|
|
||||||
p.StartAllRoutes()
|
|
||||||
})
|
|
||||||
|
|
||||||
cfg.providerInitialized = true
|
|
||||||
|
|
||||||
if pErrs.HasExtras() {
|
|
||||||
cfg.l.Error(pErrs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *config) StopProviders() {
|
|
||||||
if !cfg.providerInitialized {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.mutex.Lock()
|
|
||||||
defer cfg.mutex.Unlock()
|
|
||||||
if !cfg.providerInitialized {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ParallelForEachValue(cfg.m.Providers, (*Provider).StopAllRoutes)
|
|
||||||
cfg.m.Providers = make(map[string]*Provider)
|
|
||||||
cfg.providerInitialized = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *config) WatchChanges() {
|
|
||||||
if cfg.watcher == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cfg.watcher.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *config) StopWatching() {
|
|
||||||
if cfg.watcher == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cfg.watcher.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
type configModel struct {
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultConfig() *configModel {
|
|
||||||
return &configModel{
|
|
||||||
TimeoutShutdown: 3 * time.Second,
|
|
||||||
RedirectToHTTPS: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type config struct {
|
|
||||||
m *configModel
|
|
||||||
|
|
||||||
l logrus.FieldLogger
|
|
||||||
reader Reader
|
|
||||||
watcher Watcher
|
|
||||||
mutex sync.Mutex
|
|
||||||
providerInitialized bool
|
|
||||||
}
|
|
|
@ -1,191 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/santhosh-tekuri/jsonschema"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ImageNamePortMapTCP = map[string]string{
|
|
||||||
"postgres": "5432",
|
|
||||||
"mysql": "3306",
|
|
||||||
"mariadb": "3306",
|
|
||||||
"redis": "6379",
|
|
||||||
"mssql": "1433",
|
|
||||||
"memcached": "11211",
|
|
||||||
"rabbitmq": "5672",
|
|
||||||
"mongo": "27017",
|
|
||||||
}
|
|
||||||
ExtraNamePortMapTCP = map[string]string{
|
|
||||||
"dns": "53",
|
|
||||||
"ssh": "22",
|
|
||||||
"ftp": "21",
|
|
||||||
"smtp": "25",
|
|
||||||
"pop3": "110",
|
|
||||||
"imap": "143",
|
|
||||||
}
|
|
||||||
NamePortMapTCP = func() map[string]string {
|
|
||||||
m := make(map[string]string)
|
|
||||||
for k, v := range ImageNamePortMapTCP {
|
|
||||||
m[k] = v
|
|
||||||
}
|
|
||||||
for k, v := range ExtraNamePortMapTCP {
|
|
||||||
m[k] = v
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}()
|
|
||||||
)
|
|
||||||
|
|
||||||
var ImageNamePortMapHTTP = map[string]uint16{
|
|
||||||
"nginx": 80,
|
|
||||||
"httpd": 80,
|
|
||||||
"adguardhome": 3000,
|
|
||||||
"gogs": 3000,
|
|
||||||
"gitea": 3000,
|
|
||||||
"portainer": 9000,
|
|
||||||
"portainer-ce": 9000,
|
|
||||||
"home-assistant": 8123,
|
|
||||||
"homebridge": 8581,
|
|
||||||
"uptime-kuma": 3001,
|
|
||||||
"changedetection.io": 3000,
|
|
||||||
"prometheus": 9090,
|
|
||||||
"grafana": 3000,
|
|
||||||
"dockge": 5001,
|
|
||||||
"nginx-proxy-manager": 81,
|
|
||||||
}
|
|
||||||
|
|
||||||
var wellKnownHTTPPorts = map[uint16]bool{
|
|
||||||
80: true,
|
|
||||||
8000: true,
|
|
||||||
8008: true,
|
|
||||||
8080: true,
|
|
||||||
3000: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
StreamSchemes = []string{StreamType_TCP, StreamType_UDP} // TODO: support "tcp:udp", "udp:tcp"
|
|
||||||
HTTPSchemes = []string{"http", "https"}
|
|
||||||
ValidSchemes = append(StreamSchemes, HTTPSchemes...)
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
StreamType_UDP = "udp"
|
|
||||||
StreamType_TCP = "tcp"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProxyPathMode_Forward = "forward"
|
|
||||||
ProxyPathMode_Sub = "sub"
|
|
||||||
ProxyPathMode_RemovedPath = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProviderKind_Docker = "docker"
|
|
||||||
ProviderKind_File = "file"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: default + per proxy
|
|
||||||
var (
|
|
||||||
transport = &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
DialContext: (&net.Dialer{
|
|
||||||
Timeout: 60 * time.Second,
|
|
||||||
KeepAlive: 60 * time.Second,
|
|
||||||
}).DialContext,
|
|
||||||
MaxIdleConns: 1000,
|
|
||||||
MaxIdleConnsPerHost: 1000,
|
|
||||||
}
|
|
||||||
|
|
||||||
transportNoTLS = func() *http.Transport {
|
|
||||||
var clone = transport.Clone()
|
|
||||||
clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
||||||
return clone
|
|
||||||
}()
|
|
||||||
|
|
||||||
healthCheckHttpClient = &http.Client{
|
|
||||||
Timeout: 5 * time.Second,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
DisableKeepAlives: true,
|
|
||||||
ForceAttemptHTTP2: true,
|
|
||||||
DialContext: (&net.Dialer{
|
|
||||||
Timeout: 5 * time.Second,
|
|
||||||
KeepAlive: 5 * time.Second,
|
|
||||||
}).DialContext,
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const wildcardAlias = "*"
|
|
||||||
|
|
||||||
const clientUrlFromEnv = "FROM_ENV"
|
|
||||||
|
|
||||||
const (
|
|
||||||
certBasePath = "certs/"
|
|
||||||
certFileDefault = certBasePath + "cert.crt"
|
|
||||||
keyFileDefault = certBasePath + "priv.key"
|
|
||||||
|
|
||||||
configBasePath = "config/"
|
|
||||||
configPath = configBasePath + "config.yml"
|
|
||||||
|
|
||||||
templatesBasePath = "templates/"
|
|
||||||
panelTemplatePath = templatesBasePath + "panel/index.html"
|
|
||||||
configEditorTemplatePath = templatesBasePath + "config_editor/index.html"
|
|
||||||
|
|
||||||
schemaBasePath = "schema/"
|
|
||||||
configSchemaPath = schemaBasePath + "config.schema.json"
|
|
||||||
providersSchemaPath = schemaBasePath + "providers.schema.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
configSchema *jsonschema.Schema
|
|
||||||
providersSchema *jsonschema.Schema
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
streamStopListenTimeout = 1 * time.Second
|
|
||||||
streamDialTimeout = 3 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
const udpBufferSize = 1500
|
|
||||||
|
|
||||||
var isHostNetworkMode = getEnvBool("GOPROXY_HOST_NETWORK")
|
|
||||||
|
|
||||||
var logLevel = func() logrus.Level {
|
|
||||||
if getEnvBool("GOPROXY_DEBUG") {
|
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
|
||||||
}
|
|
||||||
return logrus.GetLevel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,295 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli/connhelper"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setConfigField(pl *ProxyLabel, c *ProxyConfig) error {
|
|
||||||
return setFieldFromSnake(c, pl.Field, pl.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
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"]
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
aliases = []string{containerName}
|
|
||||||
} else {
|
|
||||||
v, _ := commaSepParser(aliasesLabel)
|
|
||||||
aliases = v.([]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if clientIP == "" && isHostNetworkMode {
|
|
||||||
clientIP = "127.0.0.1"
|
|
||||||
}
|
|
||||||
isRemote := clientIP != ""
|
|
||||||
|
|
||||||
for _, alias := range aliases {
|
|
||||||
cfgMap[alias] = &ProxyConfig{}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
if config.Port == "0" {
|
|
||||||
l.Infof("no ports exposed, ignored")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if config.Scheme == "" {
|
|
||||||
switch {
|
|
||||||
case strings.HasSuffix(config.Port, "443"):
|
|
||||||
config.Scheme = "https"
|
|
||||||
default:
|
|
||||||
imageName := getImageName(container)
|
|
||||||
_, isKnownImage := ImageNamePortMapTCP[imageName]
|
|
||||||
if isKnownImage {
|
|
||||||
config.Scheme = "tcp"
|
|
||||||
} else {
|
|
||||||
config.Scheme = "http"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !isValidScheme(config.Scheme) {
|
|
||||||
ne.Extra("unsupported scheme").Subject(config.Scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isRemote && strings.HasPrefix(config.Port, "*") {
|
|
||||||
var err error
|
|
||||||
// find matching port
|
|
||||||
srcPort := config.Port[1:]
|
|
||||||
config.Port, err = findMatchingContainerPort(container, srcPort)
|
|
||||||
if err != nil {
|
|
||||||
ne.ExtraError(NewNestedErrorFrom(err).Subjectf("alias %s", alias))
|
|
||||||
}
|
|
||||||
if isStreamScheme(config.Scheme) {
|
|
||||||
config.Port = fmt.Sprintf("%s:%s", srcPort, config.Port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Host == "" {
|
|
||||||
switch {
|
|
||||||
case isRemote:
|
|
||||||
config.Host = clientIP
|
|
||||||
case container.HostConfig.NetworkMode == "host":
|
|
||||||
config.Host = "host.docker.internal"
|
|
||||||
case config.LoadBalance == "true", config.LoadBalance == "1":
|
|
||||||
for _, network := range container.NetworkSettings.Networks {
|
|
||||||
config.Host = network.IPAddress
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
for _, network := range container.NetworkSettings.Networks {
|
|
||||||
for _, alias := range network.Aliases {
|
|
||||||
config.Host = alias
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if config.Host == "" {
|
|
||||||
config.Host = containerName
|
|
||||||
}
|
|
||||||
config.Alias = alias
|
|
||||||
|
|
||||||
if ne.HasExtras() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cfgs = append(cfgs, *config)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ne.HasExtras() {
|
|
||||||
return nil, ne
|
|
||||||
}
|
|
||||||
return cfgs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) getDockerClient() (*client.Client, error) {
|
|
||||||
var dockerOpts []client.Opt
|
|
||||||
if p.Value == clientUrlFromEnv {
|
|
||||||
dockerOpts = []client.Opt{
|
|
||||||
client.WithHostFromEnv(),
|
|
||||||
client.WithAPIVersionNegotiation(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
helper, err := connhelper.GetConnectionHelper(p.Value)
|
|
||||||
if err != nil {
|
|
||||||
p.l.Fatal("unexpected error: ", err)
|
|
||||||
}
|
|
||||||
if helper != nil {
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: helper.Dialer,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dockerOpts = []client.Opt{
|
|
||||||
client.WithHTTPClient(httpClient),
|
|
||||||
client.WithHost(helper.Host),
|
|
||||||
client.WithAPIVersionNegotiation(),
|
|
||||||
client.WithDialContext(helper.Dialer),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dockerOpts = []client.Opt{
|
|
||||||
client.WithHost(p.Value),
|
|
||||||
client.WithAPIVersionNegotiation(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return client.NewClientWithOpts(dockerOpts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) getDockerProxyConfigs() (ProxyConfigSlice, error) {
|
|
||||||
var clientIP string
|
|
||||||
|
|
||||||
if p.Value == clientUrlFromEnv {
|
|
||||||
clientIP = ""
|
|
||||||
} else {
|
|
||||||
url, err := client.ParseHostURL(p.Value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewNestedError("invalid host url").Subject(p.Value).With(err)
|
|
||||||
}
|
|
||||||
clientIP = strings.Split(url.Host, ":")[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
dockerClient, err := p.getDockerClient()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewNestedError("unable to create docker client").With(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
|
|
||||||
containerSlice, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewNestedError("unable to list containers").With(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfgs := make(ProxyConfigSlice, 0)
|
|
||||||
|
|
||||||
ne := NewNestedError("these containers have errors")
|
|
||||||
for _, container := range containerSlice {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// var dockerUrlRegex = regexp.MustCompile(`^(?P<scheme>\w+)://(?P<host>[^:]+)(?P<port>:\d+)?(?P<path>/.*)?$`)
|
|
||||||
func getImageName(c *types.Container) string {
|
|
||||||
imageSplit := strings.Split(c.Image, "/")
|
|
||||||
imageSplit = strings.Split(imageSplit[len(imageSplit)-1], ":")
|
|
||||||
return imageSplit[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPublicPort(p types.Port) uint16 { return p.PublicPort }
|
|
||||||
func getPrivatePort(p types.Port) uint16 { return p.PrivatePort }
|
|
||||||
|
|
||||||
func selectPort(c *types.Container, isRemote bool) uint16 {
|
|
||||||
if isRemote || c.HostConfig.NetworkMode == "host" {
|
|
||||||
return selectPortInternal(c, getPublicPort)
|
|
||||||
}
|
|
||||||
return selectPortInternal(c, getPrivatePort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// used when isRemote is true
|
|
||||||
func findMatchingContainerPort(c *types.Container, ps string) (string, error) {
|
|
||||||
p, err := strconv.Atoi(ps)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
pWant := uint16(p)
|
|
||||||
for _, pGot := range c.Ports {
|
|
||||||
if pGot.PrivatePort == pWant {
|
|
||||||
return fmt.Sprintf("%d", pGot.PublicPort), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("port %d not found", p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectPortInternal(c *types.Container, getPort func(types.Port) uint16) uint16 {
|
|
||||||
imageName := getImageName(c)
|
|
||||||
// if is known image -> use known port
|
|
||||||
if port, isKnown := ImageNamePortMapHTTP[imageName]; isKnown {
|
|
||||||
for _, p := range c.Ports {
|
|
||||||
if p.PrivatePort == port {
|
|
||||||
return getPort(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if it has known http port -> use it
|
|
||||||
for _, p := range c.Ports {
|
|
||||||
if isWellKnownHTTPPort(p.PrivatePort) {
|
|
||||||
return getPort(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if it has any port -> use it
|
|
||||||
for _, p := range c.Ports {
|
|
||||||
if port := getPort(p); port != 0 {
|
|
||||||
return port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func isWellKnownHTTPPort(port uint16) bool {
|
|
||||||
_, ok := wellKnownHTTPPorts[port]
|
|
||||||
return ok
|
|
||||||
}
|
|
|
@ -1,195 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NestedError struct {
|
|
||||||
subject string
|
|
||||||
message string
|
|
||||||
extras []string
|
|
||||||
inner *NestedError
|
|
||||||
level int
|
|
||||||
|
|
||||||
sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
type NestedErrorLike interface {
|
|
||||||
Error() string
|
|
||||||
Inner() NestedErrorLike
|
|
||||||
Level() int
|
|
||||||
HasInner() bool
|
|
||||||
HasExtras() bool
|
|
||||||
|
|
||||||
Extra(string) NestedErrorLike
|
|
||||||
Extraf(string, ...any) NestedErrorLike
|
|
||||||
ExtraError(error) NestedErrorLike
|
|
||||||
Subject(string) NestedErrorLike
|
|
||||||
Subjectf(string, ...any) NestedErrorLike
|
|
||||||
With(error) NestedErrorLike
|
|
||||||
|
|
||||||
addLevel(int) NestedErrorLike
|
|
||||||
copy() *NestedError
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNestedError(message string) NestedErrorLike {
|
|
||||||
return &NestedError{message: message, extras: make([]string, 0)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNestedErrorf(format string, args ...any) NestedErrorLike {
|
|
||||||
return NewNestedError(fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) Extra(s string) NestedErrorLike {
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
if s == "" {
|
|
||||||
return ne
|
|
||||||
}
|
|
||||||
ne.Lock()
|
|
||||||
defer ne.Unlock()
|
|
||||||
ne.extras = append(ne.extras, s)
|
|
||||||
return ne
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) Extraf(format string, args ...any) NestedErrorLike {
|
|
||||||
return ne.Extra(fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) ExtraError(e error) NestedErrorLike {
|
|
||||||
switch t := e.(type) {
|
|
||||||
case NestedErrorLike:
|
|
||||||
extra := t.copy()
|
|
||||||
extra.addLevel(ne.Level() + 1)
|
|
||||||
e = extra
|
|
||||||
}
|
|
||||||
return ne.Extra(e.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) Subject(s string) NestedErrorLike {
|
|
||||||
ne.subject = s
|
|
||||||
return ne
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) Subjectf(format string, args ...any) NestedErrorLike {
|
|
||||||
ne.subject = fmt.Sprintf(format, args...)
|
|
||||||
return ne
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) Inner() NestedErrorLike {
|
|
||||||
return ne.inner
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) Level() int {
|
|
||||||
return ne.level
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) Error() string {
|
|
||||||
var buf strings.Builder
|
|
||||||
ne.writeToSB(&buf, ne.level, "")
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) HasInner() bool {
|
|
||||||
return ne.inner != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) HasExtras() bool {
|
|
||||||
return len(ne.extras) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) With(inner error) NestedErrorLike {
|
|
||||||
ne.Lock()
|
|
||||||
defer ne.Unlock()
|
|
||||||
|
|
||||||
var in *NestedError
|
|
||||||
|
|
||||||
switch t := inner.(type) {
|
|
||||||
case NestedErrorLike:
|
|
||||||
in = t.copy()
|
|
||||||
default:
|
|
||||||
in = &NestedError{message: t.Error()}
|
|
||||||
}
|
|
||||||
if ne.inner == nil {
|
|
||||||
ne.inner = in
|
|
||||||
} else {
|
|
||||||
ne.inner.ExtraError(in)
|
|
||||||
}
|
|
||||||
root := ne
|
|
||||||
for root.inner != nil {
|
|
||||||
root.inner.level = root.level + 1
|
|
||||||
root = root.inner
|
|
||||||
}
|
|
||||||
return ne
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) addLevel(level int) NestedErrorLike {
|
|
||||||
ne.level += level
|
|
||||||
if ne.inner != nil {
|
|
||||||
ne.inner.addLevel(level)
|
|
||||||
}
|
|
||||||
return ne
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) copy() *NestedError {
|
|
||||||
var inner *NestedError
|
|
||||||
if ne.inner != nil {
|
|
||||||
inner = ne.inner.copy()
|
|
||||||
}
|
|
||||||
return &NestedError{
|
|
||||||
subject: ne.subject,
|
|
||||||
message: ne.message,
|
|
||||||
extras: ne.extras,
|
|
||||||
inner: inner,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) writeIndents(sb *strings.Builder, level int) {
|
|
||||||
for i := 0; i < level; i++ {
|
|
||||||
sb.WriteString(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ne *NestedError) writeToSB(sb *strings.Builder, level int, prefix string) {
|
|
||||||
ne.writeIndents(sb, level)
|
|
||||||
sb.WriteString(prefix)
|
|
||||||
|
|
||||||
if ne.subject != "" {
|
|
||||||
sb.WriteString(ne.subject)
|
|
||||||
if ne.message != "" {
|
|
||||||
sb.WriteString(": ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ne.message != "" {
|
|
||||||
sb.WriteString(ne.message)
|
|
||||||
}
|
|
||||||
if ne.HasExtras() || ne.HasInner() {
|
|
||||||
sb.WriteString(":\n")
|
|
||||||
}
|
|
||||||
level += 1
|
|
||||||
for _, l := range ne.extras {
|
|
||||||
if l == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ne.writeIndents(sb, level)
|
|
||||||
sb.WriteString("- ")
|
|
||||||
sb.WriteString(l)
|
|
||||||
sb.WriteRune('\n')
|
|
||||||
}
|
|
||||||
if ne.inner != nil {
|
|
||||||
ne.inner.writeToSB(sb, level, "- ")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *Provider) GetFilePath() string {
|
|
||||||
return path.Join(configBasePath, p.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) ValidateFile() (ProxyConfigSlice, error) {
|
|
||||||
path := p.GetFilePath()
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewNestedError("unable to read providers file").Subject(path).With(err)
|
|
||||||
}
|
|
||||||
result, err := ValidateFileContent(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewNestedError(err.Error()).Subject(path)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateFileContent(data []byte) (ProxyConfigSlice, error) {
|
|
||||||
configMap := make(ProxyConfigMap, 0)
|
|
||||||
if err := yaml.Unmarshal(data, &configMap); err != nil {
|
|
||||||
return nil, NewNestedError("invalid yaml").With(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ne := NewNestedError("errors in providers")
|
|
||||||
|
|
||||||
configs := make(ProxyConfigSlice, len(configMap))
|
|
||||||
i := 0
|
|
||||||
for alias, cfg := range configMap {
|
|
||||||
cfg.Alias = alias
|
|
||||||
if err := cfg.SetDefaults(); err != nil {
|
|
||||||
ne.ExtraError(err)
|
|
||||||
} else {
|
|
||||||
configs[i] = cfg
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateYaml(providersSchema, data); err != nil {
|
|
||||||
ne.ExtraError(err)
|
|
||||||
}
|
|
||||||
if ne.HasExtras() {
|
|
||||||
return nil, ne
|
|
||||||
}
|
|
||||||
return configs, nil
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
func ParallelForEach[T interface{}](obj []T, do func(T)) {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(len(obj))
|
|
||||||
for _, v := range obj {
|
|
||||||
go func(v T) {
|
|
||||||
do(v)
|
|
||||||
wg.Done()
|
|
||||||
}(v)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParallelForEachValue[K comparable, V interface{}](obj map[K]V, do func(V)) {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(len(obj))
|
|
||||||
for _, v := range obj {
|
|
||||||
go func(v V) {
|
|
||||||
do(v)
|
|
||||||
wg.Done()
|
|
||||||
}(v)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParallelForEachKeyValue[K comparable, V interface{}](obj map[K]V, do func(K, V)) {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(len(obj))
|
|
||||||
for k, v := range obj {
|
|
||||||
go func(k K, v V) {
|
|
||||||
do(k, v)
|
|
||||||
wg.Done()
|
|
||||||
}(k, v)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import "sync/atomic"
|
|
||||||
|
|
||||||
type httpLoadBalancePool struct {
|
|
||||||
pool []*HTTPRoute
|
|
||||||
curentIndex atomic.Int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTPLoadBalancePool() *httpLoadBalancePool {
|
|
||||||
return &httpLoadBalancePool{
|
|
||||||
pool: make([]*HTTPRoute, 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *httpLoadBalancePool) Add(route *HTTPRoute) {
|
|
||||||
p.pool = append(p.pool, route)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *httpLoadBalancePool) Iterator() []*HTTPRoute {
|
|
||||||
return p.pool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *httpLoadBalancePool) Pick() *HTTPRoute {
|
|
||||||
// round-robin
|
|
||||||
index := int(p.curentIndex.Load())
|
|
||||||
defer p.curentIndex.Add(1)
|
|
||||||
return p.pool[index%len(p.pool)]
|
|
||||||
}
|
|
|
@ -1,162 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HTTPRoute struct {
|
|
||||||
Alias string
|
|
||||||
Url *url.URL
|
|
||||||
Path string
|
|
||||||
PathMode string
|
|
||||||
Proxy *ReverseProxy
|
|
||||||
|
|
||||||
l logrus.FieldLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) {
|
|
||||||
u := fmt.Sprintf("%s://%s:%s", config.Scheme, config.Host, config.Port)
|
|
||||||
url, err := url.Parse(u)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewNestedErrorf("invalid url").Subject(u).With(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var tr *http.Transport
|
|
||||||
if config.NoTLSVerify {
|
|
||||||
tr = transportNoTLS
|
|
||||||
} else {
|
|
||||||
tr = transport
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy := NewReverseProxy(url, tr, config)
|
|
||||||
|
|
||||||
route := &HTTPRoute{
|
|
||||||
Alias: config.Alias,
|
|
||||||
Url: url,
|
|
||||||
Path: config.Path,
|
|
||||||
Proxy: proxy,
|
|
||||||
PathMode: config.PathMode,
|
|
||||||
l: logrus.WithField("alias", config.Alias),
|
|
||||||
}
|
|
||||||
|
|
||||||
var rewriteBegin = proxy.Rewrite
|
|
||||||
var rewrite func(*ProxyRequest)
|
|
||||||
var modifyResponse func(*http.Response) error
|
|
||||||
|
|
||||||
// no path or forward path
|
|
||||||
if config.Path == "" || config.PathMode == ProxyPathMode_Forward {
|
|
||||||
rewrite = rewriteBegin
|
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if logLevel == logrus.DebugLevel {
|
|
||||||
route.Proxy.Rewrite = func(pr *ProxyRequest) {
|
|
||||||
rewrite(pr)
|
|
||||||
route.l.Debug("request URL: ", pr.In.Host, pr.In.URL.Path)
|
|
||||||
route.l.Debug("request headers: ", pr.In.Header)
|
|
||||||
}
|
|
||||||
route.Proxy.ModifyResponse = func(r *http.Response) error {
|
|
||||||
route.l.Debug("response URL: ", r.Request.URL.String())
|
|
||||||
route.l.Debug("response headers: ", r.Header)
|
|
||||||
if modifyResponse != nil {
|
|
||||||
return modifyResponse(r)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
route.Proxy.Rewrite = rewrite
|
|
||||||
}
|
|
||||||
|
|
||||||
return route, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *HTTPRoute) Start() {
|
|
||||||
httpRoutes.Get(r.Alias).Add(r.Path, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *HTTPRoute) Stop() {
|
|
||||||
httpRoutes.Delete(r.Alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
func redirectToTLSHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Redirect to the same host but with HTTPS
|
|
||||||
var redirectCode int
|
|
||||||
if r.Method == http.MethodGet {
|
|
||||||
redirectCode = http.StatusMovedPermanently
|
|
||||||
} else {
|
|
||||||
redirectCode = http.StatusPermanentRedirect
|
|
||||||
}
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("https://%s%s?%s", r.Host, r.URL.Path, r.URL.RawQuery), redirectCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func findHTTPRoute(host string, path string) (*HTTPRoute, error) {
|
|
||||||
subdomain := strings.Split(host, ".")[0]
|
|
||||||
routeMap, ok := httpRoutes.UnsafeGet(subdomain)
|
|
||||||
if ok {
|
|
||||||
return routeMap.FindMatch(path)
|
|
||||||
}
|
|
||||||
return nil, NewNestedError("no matching route for subdomain").Subject(subdomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
route, err := findHTTPRoute(r.Host, r.URL.Path)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "404 Not Found", http.StatusNotFound)
|
|
||||||
err = NewNestedError("request failed").
|
|
||||||
Subjectf("%s %s%s", r.Method, r.Host, r.URL.Path).
|
|
||||||
With(err)
|
|
||||||
logrus.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
route.Proxy.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config *ProxyConfig) pathSubModResp(r *http.Response) error {
|
|
||||||
contentType, ok := r.Header["Content-Type"]
|
|
||||||
if !ok || len(contentType) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// disable cache
|
|
||||||
r.Header.Set("Cache-Control", "no-store")
|
|
||||||
|
|
||||||
var err error = nil
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(contentType[0], "text/html"):
|
|
||||||
err = utils.respHTMLSubPath(r, config.Path)
|
|
||||||
case strings.HasPrefix(contentType[0], "application/javascript"):
|
|
||||||
err = utils.respJSSubPath(r, config.Path)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
err = NewNestedError("failed to remove path prefix").Subject(config.Path).With(err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// alias -> (path -> routes)
|
|
||||||
type HTTPRoutes SafeMap[string, pathPoolMap]
|
|
||||||
|
|
||||||
var httpRoutes HTTPRoutes = NewSafeMapOf[HTTPRoutes](newPathPoolMap)
|
|
|
@ -1,127 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Reader interface {
|
|
||||||
Read() ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileReader struct {
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FileReader) Read() ([]byte, error) {
|
|
||||||
return os.ReadFile(r.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ByteReader struct {
|
|
||||||
Data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ByteReader) Read() ([]byte, error) {
|
|
||||||
return r.Data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReadCloser struct {
|
|
||||||
ctx context.Context
|
|
||||||
r io.ReadCloser
|
|
||||||
closed atomic.Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ReadCloser) Read(p []byte) (int, error) {
|
|
||||||
select {
|
|
||||||
case <-r.ctx.Done():
|
|
||||||
return 0, r.ctx.Err()
|
|
||||||
default:
|
|
||||||
return r.r.Read(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ReadCloser) Close() error {
|
|
||||||
if r.closed.Load() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
r.closed.Store(true)
|
|
||||||
return r.r.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Pipe struct {
|
|
||||||
r ReadCloser
|
|
||||||
w io.WriteCloser
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPipe(ctx context.Context, r io.ReadCloser, w io.WriteCloser) *Pipe {
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
return &Pipe{
|
|
||||||
r: ReadCloser{ctx: ctx, r: r},
|
|
||||||
w: w,
|
|
||||||
ctx: ctx,
|
|
||||||
cancel: cancel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pipe) Start() error {
|
|
||||||
return Copy(p.ctx, p.w, &p.r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pipe) Stop() error {
|
|
||||||
p.cancel()
|
|
||||||
return errors.Join(fmt.Errorf("read: %w", p.r.Close()), fmt.Errorf("write: %w", p.w.Close()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pipe) Write(b []byte) (int, error) {
|
|
||||||
return p.w.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
type BidirectionalPipe struct {
|
|
||||||
pSrcDst Pipe
|
|
||||||
pDstSrc Pipe
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBidirectionalPipe(ctx context.Context, rw1 io.ReadWriteCloser, rw2 io.ReadWriteCloser) *BidirectionalPipe {
|
|
||||||
return &BidirectionalPipe{
|
|
||||||
pSrcDst: *NewPipe(ctx, rw1, rw2),
|
|
||||||
pDstSrc: *NewPipe(ctx, rw2, rw1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBidirectionalPipeIntermediate(ctx context.Context, listener io.ReadCloser, client io.ReadWriteCloser, target io.ReadWriteCloser) *BidirectionalPipe {
|
|
||||||
return &BidirectionalPipe{
|
|
||||||
pSrcDst: *NewPipe(ctx, listener, client),
|
|
||||||
pDstSrc: *NewPipe(ctx, client, target),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *BidirectionalPipe) Start() error {
|
|
||||||
errCh := make(chan error, 2)
|
|
||||||
go func() {
|
|
||||||
errCh <- p.pSrcDst.Start()
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
errCh <- p.pDstSrc.Start()
|
|
||||||
}()
|
|
||||||
for err := range errCh {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *BidirectionalPipe) Stop() error {
|
|
||||||
return errors.Join(p.pSrcDst.Stop(), p.pDstSrc.Stop())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Copy(ctx context.Context, dst io.WriteCloser, src io.ReadCloser) error {
|
|
||||||
_, err := io.Copy(dst, &ReadCloser{ctx: ctx, r: src})
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
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")
|
|
|
@ -1,137 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var cfg Config
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
|
||||||
|
|
||||||
args := getArgs()
|
|
||||||
|
|
||||||
if isRunningAsService {
|
|
||||||
logrus.SetFormatter(&logrus.TextFormatter{
|
|
||||||
DisableColors: true,
|
|
||||||
DisableTimestamp: true,
|
|
||||||
DisableSorting: true,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
logrus.SetFormatter(&logrus.TextFormatter{
|
|
||||||
ForceColors: true,
|
|
||||||
DisableColors: false,
|
|
||||||
DisableSorting: true,
|
|
||||||
FullTimestamp: true,
|
|
||||||
TimestampFormat: "01-02 15:04:05",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.Command == CommandReload {
|
|
||||||
err := utils.reloadServer()
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
initSchema()
|
|
||||||
|
|
||||||
cfg = NewConfig(configPath)
|
|
||||||
cfg.MustLoad()
|
|
||||||
|
|
||||||
if args.Command == CommandValidate {
|
|
||||||
logrus.Printf("config OK")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
autoCertProvider, err := cfg.GetAutoCertProvider()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
aclog.Warn(err)
|
|
||||||
autoCertProvider = nil // TODO: remove, it is expected to be nil if error is not nil, but it is not for now
|
|
||||||
}
|
|
||||||
|
|
||||||
var proxyServer *Server
|
|
||||||
var panelServer *Server
|
|
||||||
|
|
||||||
if autoCertProvider != nil {
|
|
||||||
ok := autoCertProvider.LoadCert()
|
|
||||||
if !ok {
|
|
||||||
if ne := autoCertProvider.ObtainCert(); ne != nil {
|
|
||||||
aclog.Fatal(ne)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for name, expiry := range autoCertProvider.GetExpiries() {
|
|
||||||
aclog.Infof("certificate %q: expire on %v", name, expiry)
|
|
||||||
}
|
|
||||||
go autoCertProvider.ScheduleRenewal()
|
|
||||||
}
|
|
||||||
proxyServer = NewServer(ServerOptions{
|
|
||||||
Name: "proxy",
|
|
||||||
CertProvider: autoCertProvider,
|
|
||||||
HTTPAddr: ":80",
|
|
||||||
HTTPSAddr: ":443",
|
|
||||||
Handler: http.HandlerFunc(proxyHandler),
|
|
||||||
RedirectToHTTPS: cfg.Value().RedirectToHTTPS,
|
|
||||||
})
|
|
||||||
panelServer = NewServer(ServerOptions{
|
|
||||||
Name: "panel",
|
|
||||||
CertProvider: autoCertProvider,
|
|
||||||
HTTPAddr: ":8080",
|
|
||||||
HTTPSAddr: ":8443",
|
|
||||||
Handler: panelHandler,
|
|
||||||
RedirectToHTTPS: cfg.Value().RedirectToHTTPS,
|
|
||||||
})
|
|
||||||
|
|
||||||
proxyServer.Start()
|
|
||||||
panelServer.Start()
|
|
||||||
|
|
||||||
InitFSWatcher()
|
|
||||||
|
|
||||||
cfg.StartProviders()
|
|
||||||
cfg.WatchChanges()
|
|
||||||
|
|
||||||
sig := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sig, syscall.SIGINT)
|
|
||||||
signal.Notify(sig, syscall.SIGTERM)
|
|
||||||
signal.Notify(sig, syscall.SIGHUP)
|
|
||||||
|
|
||||||
<-sig
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,98 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
type safeMap[KT comparable, VT interface{}] struct {
|
|
||||||
SafeMap[KT, VT]
|
|
||||||
m map[KT]VT
|
|
||||||
defaultFactory func() VT
|
|
||||||
sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
type SafeMap[KT comparable, VT interface{}] interface {
|
|
||||||
Set(key KT, value VT)
|
|
||||||
Ensure(key KT)
|
|
||||||
Get(key KT) VT
|
|
||||||
UnsafeGet(key KT) (VT, bool)
|
|
||||||
Delete(key KT)
|
|
||||||
Clear()
|
|
||||||
Size() int
|
|
||||||
Contains(key KT) bool
|
|
||||||
ForEach(fn func(key KT, value VT))
|
|
||||||
Iterator() map[KT]VT
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSafeMapOf[T SafeMap[KT, VT], KT comparable, VT interface{}](df ...func() VT) SafeMap[KT, VT] {
|
|
||||||
if len(df) == 0 {
|
|
||||||
return &safeMap[KT, VT]{
|
|
||||||
m: make(map[KT]VT),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &safeMap[KT, VT]{
|
|
||||||
m: make(map[KT]VT),
|
|
||||||
defaultFactory: df[0],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *safeMap[KT, VT]) Set(key KT, value VT) {
|
|
||||||
m.Lock()
|
|
||||||
m.m[key] = value
|
|
||||||
m.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *safeMap[KT, VT]) Ensure(key KT) {
|
|
||||||
m.Lock()
|
|
||||||
if _, ok := m.m[key]; !ok {
|
|
||||||
m.m[key] = m.defaultFactory()
|
|
||||||
}
|
|
||||||
m.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *safeMap[KT, VT]) Get(key KT) VT {
|
|
||||||
m.RLock()
|
|
||||||
value := m.m[key]
|
|
||||||
m.RUnlock()
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *safeMap[KT, VT]) UnsafeGet(key KT) (VT, bool) {
|
|
||||||
value, ok := m.m[key]
|
|
||||||
return value, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *safeMap[KT, VT]) Delete(key KT) {
|
|
||||||
m.Lock()
|
|
||||||
delete(m.m, key)
|
|
||||||
m.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *safeMap[KT, VT]) Clear() {
|
|
||||||
m.Lock()
|
|
||||||
m.m = make(map[KT]VT)
|
|
||||||
m.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *safeMap[KT, VT]) Size() int {
|
|
||||||
m.RLock()
|
|
||||||
defer m.RUnlock()
|
|
||||||
return len(m.m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *safeMap[KT, VT]) Contains(key KT) bool {
|
|
||||||
m.RLock()
|
|
||||||
_, ok := m.m[key]
|
|
||||||
m.RUnlock()
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *safeMap[KT, VT]) ForEach(fn func(key KT, value VT)) {
|
|
||||||
m.RLock()
|
|
||||||
for k, v := range m.m {
|
|
||||||
fn(k, v)
|
|
||||||
}
|
|
||||||
m.RUnlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *safeMap[KT, VT]) Iterator() map[KT]VT {
|
|
||||||
return m.m
|
|
||||||
}
|
|
|
@ -1,153 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
var panelHandler = panelRouter()
|
|
||||||
|
|
||||||
func panelRouter() *http.ServeMux {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("GET /{$}", panelServeFile)
|
|
||||||
mux.HandleFunc("GET /{file}", panelServeFile)
|
|
||||||
mux.HandleFunc("GET /panel/", panelPage)
|
|
||||||
mux.HandleFunc("GET /panel/{file}", panelServeFile)
|
|
||||||
mux.HandleFunc("HEAD /checkhealth", panelCheckTargetHealth)
|
|
||||||
mux.HandleFunc("GET /config_editor/", panelConfigEditor)
|
|
||||||
mux.HandleFunc("GET /config_editor/{file}", panelServeFile)
|
|
||||||
mux.HandleFunc("GET /config/{file}", panelConfigGet)
|
|
||||||
mux.HandleFunc("PUT /config/{file}", panelConfigUpdate)
|
|
||||||
mux.HandleFunc("POST /reload", configReload)
|
|
||||||
mux.HandleFunc("GET /codemirror/", panelServeFile)
|
|
||||||
return mux
|
|
||||||
}
|
|
||||||
|
|
||||||
func panelPage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
resp := struct {
|
|
||||||
HTTPRoutes HTTPRoutes
|
|
||||||
StreamRoutes StreamRoutes
|
|
||||||
}{httpRoutes, streamRoutes}
|
|
||||||
|
|
||||||
panelRenderFile(w, r, panelTemplatePath, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func panelCheckTargetHealth(w http.ResponseWriter, r *http.Request) {
|
|
||||||
targetUrl := r.URL.Query().Get("target")
|
|
||||||
|
|
||||||
if targetUrl == "" {
|
|
||||||
panelHandleErr(w, r, errors.New("target is required"), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
url, err := url.Parse(targetUrl)
|
|
||||||
if err != nil {
|
|
||||||
err = NewNestedError("failed to parse url").Subject(targetUrl).With(err)
|
|
||||||
panelHandleErr(w, r, err, http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
scheme := url.Scheme
|
|
||||||
|
|
||||||
if isStreamScheme(scheme) {
|
|
||||||
err = utils.healthCheckStream(scheme, url.Host)
|
|
||||||
} else {
|
|
||||||
err = utils.healthCheckHttp(targetUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
} else {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func panelConfigEditor(w http.ResponseWriter, r *http.Request) {
|
|
||||||
cfgFiles := make([]string, 0)
|
|
||||||
cfgFiles = append(cfgFiles, path.Base(configPath))
|
|
||||||
for _, p := range cfg.Value().Providers {
|
|
||||||
if p.Kind != ProviderKind_File {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cfgFiles = append(cfgFiles, p.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
panelRenderFile(w, r, configEditorTemplatePath, cfgFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
func panelConfigGet(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.ServeFile(w, r, path.Join(configBasePath, r.PathValue("file")))
|
|
||||||
}
|
|
||||||
|
|
||||||
func panelConfigUpdate(w http.ResponseWriter, r *http.Request) {
|
|
||||||
p := r.PathValue("file")
|
|
||||||
content := make([]byte, r.ContentLength)
|
|
||||||
_, err := r.Body.Read(content)
|
|
||||||
if err != nil {
|
|
||||||
panelHandleErr(w, r, NewNestedError("unable to read request body").Subject(p).With(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if p == path.Base(configPath) {
|
|
||||||
err = ValidateConfig(content)
|
|
||||||
} else {
|
|
||||||
_, err = ValidateFileContent(content)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
panelHandleErr(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
http.ServeFile(w, r, path.Join(templatesBasePath, r.URL.Path))
|
|
||||||
}
|
|
||||||
|
|
||||||
func panelRenderFile(w http.ResponseWriter, r *http.Request, f string, data any) {
|
|
||||||
tmpl, err := template.ParseFiles(f)
|
|
||||||
if err != nil {
|
|
||||||
panelHandleErr(w, r, NewNestedError("unable to parse template").With(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tmpl.Execute(w, data)
|
|
||||||
if err != nil {
|
|
||||||
panelHandleErr(w, r, NewNestedError("unable to render template").With(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func configReload(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := cfg.Reload()
|
|
||||||
if err != nil {
|
|
||||||
panelHandleErr(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func panelHandleErr(w http.ResponseWriter, r *http.Request, err error, code ...int) {
|
|
||||||
err = NewNestedErrorFrom(err).Subjectf("%s %s", r.Method, r.URL)
|
|
||||||
palog.Error(err)
|
|
||||||
if len(code) > 0 {
|
|
||||||
http.Error(w, err.Error(), code[0])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type pathPoolMap struct {
|
|
||||||
SafeMap[string, *httpLoadBalancePool]
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPathPoolMap() pathPoolMap {
|
|
||||||
return pathPoolMap{NewSafeMapOf[pathPoolMap](NewHTTPLoadBalancePool)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m pathPoolMap) Add(path string, route *HTTPRoute) {
|
|
||||||
m.Ensure(path)
|
|
||||||
m.Get(path).Add(route)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m pathPoolMap) FindMatch(pathGot string) (*HTTPRoute, NestedErrorLike) {
|
|
||||||
for pathWant, v := range m.Iterator() {
|
|
||||||
if strings.HasPrefix(pathGot, pathWant) {
|
|
||||||
return v.Pick(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, NewNestedError("no matching path").Subject(pathGot)
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Provider struct {
|
|
||||||
Kind string `json:"kind"` // docker, file
|
|
||||||
Value string `json:"value"`
|
|
||||||
|
|
||||||
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 = logrus.WithField("provider", name)
|
|
||||||
p.reloadReqCh = make(chan struct{}, 1)
|
|
||||||
|
|
||||||
defer p.initWatcher()
|
|
||||||
|
|
||||||
if err := p.loadProxyConfig(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) StartAllRoutes() {
|
|
||||||
ParallelForEachValue(p.routes, Route.Start)
|
|
||||||
p.watcher.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) StopAllRoutes() {
|
|
||||||
p.watcher.Stop()
|
|
||||||
ParallelForEachValue(p.routes, Route.Stop)
|
|
||||||
p.routes = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) ReloadRoutes() {
|
|
||||||
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)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
p.StartAllRoutes()
|
|
||||||
default:
|
|
||||||
p.l.Info("reload request already in progress")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) loadProxyConfig() error {
|
|
||||||
var cfgs ProxyConfigSlice
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch p.Kind {
|
|
||||||
case ProviderKind_Docker:
|
|
||||||
cfgs, err = p.getDockerProxyConfigs()
|
|
||||||
case ProviderKind_File:
|
|
||||||
cfgs, err = p.ValidateFile()
|
|
||||||
default:
|
|
||||||
// this line should never be reached
|
|
||||||
return NewNestedError("unknown provider kind")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.l.Infof("loaded %d proxy configurations", len(cfgs))
|
|
||||||
|
|
||||||
p.routes = make(map[string]Route, len(cfgs))
|
|
||||||
pErrs := NewNestedError("failed to create these routes")
|
|
||||||
|
|
||||||
for _, cfg := range cfgs {
|
|
||||||
r, err := NewRoute(&cfg)
|
|
||||||
if err != nil {
|
|
||||||
pErrs.ExtraError(NewNestedErrorFrom(err).Subject(cfg.Alias))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
p.routes[cfg.GetID()] = r
|
|
||||||
}
|
|
||||||
|
|
||||||
if pErrs.HasExtras() {
|
|
||||||
p.routes = nil
|
|
||||||
return pErrs
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) initWatcher() error {
|
|
||||||
switch p.Kind {
|
|
||||||
case ProviderKind_Docker:
|
|
||||||
dockerClient, err := p.getDockerClient()
|
|
||||||
if err != nil {
|
|
||||||
return NewNestedError("unable to create docker client").With(err)
|
|
||||||
}
|
|
||||||
p.watcher = p.NewDockerWatcher(dockerClient)
|
|
||||||
case ProviderKind_File:
|
|
||||||
p.watcher = p.NewFileWatcher()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
// used by `GetFileProxyConfigs`
|
|
||||||
func (cfg *ProxyConfig) SetDefaults() error {
|
|
||||||
err := NewNestedError("invalid proxy config").Subject(cfg.Alias)
|
|
||||||
|
|
||||||
if cfg.Alias == "" {
|
|
||||||
err.Extra("alias is required")
|
|
||||||
}
|
|
||||||
if cfg.Scheme == "" {
|
|
||||||
cfg.Scheme = "http"
|
|
||||||
}
|
|
||||||
if cfg.Host == "" {
|
|
||||||
err.Extra("host is required")
|
|
||||||
}
|
|
||||||
if cfg.Port == "" {
|
|
||||||
switch cfg.Scheme {
|
|
||||||
case "http":
|
|
||||||
cfg.Port = "80"
|
|
||||||
case "https":
|
|
||||||
cfg.Port = "443"
|
|
||||||
default:
|
|
||||||
err.Extraf("port is required for %s scheme", cfg.Scheme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err.HasExtras() {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *ProxyConfig) GetID() string {
|
|
||||||
return fmt.Sprintf("%s-%s-%s-%s-%s", cfg.Alias, cfg.Scheme, cfg.Host, cfg.Port, cfg.Path)
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
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,
|
|
||||||
}
|
|
|
@ -1,186 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
type Route interface {
|
|
||||||
Start()
|
|
||||||
Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRoute(cfg *ProxyConfig) (Route, error) {
|
|
||||||
if isStreamScheme(cfg.Scheme) {
|
|
||||||
id := cfg.GetID()
|
|
||||||
if streamRoutes.Contains(id) {
|
|
||||||
return nil, NewNestedError("duplicated stream").Subject(cfg.Alias)
|
|
||||||
}
|
|
||||||
route, err := NewStreamRoute(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewNestedErrorFrom(err).Subject(cfg.Alias)
|
|
||||||
}
|
|
||||||
return route, nil
|
|
||||||
} else {
|
|
||||||
httpRoutes.Ensure(cfg.Alias)
|
|
||||||
route, err := NewHTTPRoute(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewNestedErrorFrom(err).Subject(cfg.Alias)
|
|
||||||
}
|
|
||||||
return route, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidScheme(s string) bool {
|
|
||||||
for _, v := range ValidSchemes {
|
|
||||||
if v == s {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isStreamScheme(s string) bool {
|
|
||||||
for _, v := range StreamSchemes {
|
|
||||||
if v == s {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Server struct {
|
|
||||||
Name string
|
|
||||||
KeyFile string
|
|
||||||
CertFile string
|
|
||||||
CertProvider AutoCertProvider
|
|
||||||
http *http.Server
|
|
||||||
https *http.Server
|
|
||||||
httpStarted bool
|
|
||||||
httpsStarted bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerOptions struct {
|
|
||||||
Name string
|
|
||||||
HTTPAddr string
|
|
||||||
HTTPSAddr string
|
|
||||||
CertProvider AutoCertProvider
|
|
||||||
RedirectToHTTPS bool
|
|
||||||
Handler http.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogrusWrapper struct {
|
|
||||||
*logrus.Entry
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l LogrusWrapper) Write(b []byte) (int, error) {
|
|
||||||
return l.Logger.WriterLevel(logrus.ErrorLevel).Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServer(opt ServerOptions) *Server {
|
|
||||||
var httpHandler http.Handler
|
|
||||||
var s *Server
|
|
||||||
if opt.RedirectToHTTPS {
|
|
||||||
httpHandler = http.HandlerFunc(redirectToTLSHandler)
|
|
||||||
} else {
|
|
||||||
httpHandler = opt.Handler
|
|
||||||
}
|
|
||||||
logger := log.Default()
|
|
||||||
logger.SetOutput(LogrusWrapper{
|
|
||||||
logrus.WithFields(logrus.Fields{"component": "server", "name": opt.Name}),
|
|
||||||
})
|
|
||||||
if opt.CertProvider != nil {
|
|
||||||
s = &Server{
|
|
||||||
Name: opt.Name,
|
|
||||||
CertProvider: opt.CertProvider,
|
|
||||||
http: &http.Server{
|
|
||||||
Addr: opt.HTTPAddr,
|
|
||||||
Handler: httpHandler,
|
|
||||||
ErrorLog: logger,
|
|
||||||
},
|
|
||||||
https: &http.Server{
|
|
||||||
Addr: opt.HTTPSAddr,
|
|
||||||
Handler: opt.Handler,
|
|
||||||
ErrorLog: logger,
|
|
||||||
TLSConfig: &tls.Config{
|
|
||||||
GetCertificate: opt.CertProvider.GetCert,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s = &Server{
|
|
||||||
Name: opt.Name,
|
|
||||||
KeyFile: keyFileDefault,
|
|
||||||
CertFile: certFileDefault,
|
|
||||||
http: &http.Server{
|
|
||||||
Addr: opt.HTTPAddr,
|
|
||||||
Handler: httpHandler,
|
|
||||||
ErrorLog: logger,
|
|
||||||
},
|
|
||||||
https: &http.Server{
|
|
||||||
Addr: opt.HTTPSAddr,
|
|
||||||
Handler: opt.Handler,
|
|
||||||
ErrorLog: logger,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if !s.certsOK() {
|
|
||||||
s.http.Handler = opt.Handler
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) Start() {
|
|
||||||
if s.http != nil {
|
|
||||||
s.httpStarted = true
|
|
||||||
logrus.Printf("starting http %s server on %s", s.Name, s.http.Addr)
|
|
||||||
go func() {
|
|
||||||
err := s.http.ListenAndServe()
|
|
||||||
s.handleErr("http", err)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.https != nil && (s.CertProvider != nil || s.certsOK()) {
|
|
||||||
s.httpsStarted = true
|
|
||||||
logrus.Printf("starting https %s server on %s", s.Name, s.https.Addr)
|
|
||||||
go func() {
|
|
||||||
err := s.https.ListenAndServeTLS(s.CertFile, s.KeyFile)
|
|
||||||
s.handleErr("https", err)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) Stop() {
|
|
||||||
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
|
|
||||||
|
|
||||||
if s.httpStarted {
|
|
||||||
errHTTP := s.http.Shutdown(ctx)
|
|
||||||
s.handleErr("http", errHTTP)
|
|
||||||
s.httpStarted = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.httpsStarted {
|
|
||||||
errHTTPS := s.https.Shutdown(ctx)
|
|
||||||
s.handleErr("https", errHTTPS)
|
|
||||||
s.httpsStarted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleErr(scheme string, err error) {
|
|
||||||
switch err {
|
|
||||||
case nil, http.ErrServerClosed:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
logrus.Fatalf("failed to start %s %s server: %v", scheme, s.Name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) certsOK() bool {
|
|
||||||
return utils.fileOK(s.CertFile) && utils.fileOK(s.KeyFile)
|
|
||||||
}
|
|
|
@ -1,242 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StreamImpl interface {
|
|
||||||
Setup() error
|
|
||||||
Accept() (interface{}, error)
|
|
||||||
Handle(interface{}) error
|
|
||||||
CloseListeners()
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamRoute interface {
|
|
||||||
Route
|
|
||||||
ListeningUrl() string
|
|
||||||
TargetUrl() string
|
|
||||||
Logger() logrus.FieldLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamRouteBase struct {
|
|
||||||
Alias string // to show in panel
|
|
||||||
Type string
|
|
||||||
ListeningScheme string
|
|
||||||
ListeningPort int
|
|
||||||
TargetScheme string
|
|
||||||
TargetHost string
|
|
||||||
TargetPort int
|
|
||||||
|
|
||||||
id string
|
|
||||||
wg sync.WaitGroup
|
|
||||||
stopCh chan struct{}
|
|
||||||
connCh chan interface{}
|
|
||||||
started bool
|
|
||||||
l logrus.FieldLogger
|
|
||||||
|
|
||||||
StreamImpl
|
|
||||||
}
|
|
||||||
|
|
||||||
func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
|
|
||||||
var streamType string = StreamType_TCP
|
|
||||||
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 {
|
|
||||||
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]
|
|
||||||
dstPort = portSplit[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if port, hasName := NamePortMapTCP[dstPort]; hasName {
|
|
||||||
dstPort = port
|
|
||||||
}
|
|
||||||
|
|
||||||
srcPortInt, err := strconv.Atoi(srcPort)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewNestedError("invalid stream source port").Subject(srcPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.markPortInUse(srcPortInt)
|
|
||||||
|
|
||||||
dstPortInt, err := strconv.Atoi(dstPort)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewNestedError("invalid stream target port").Subject(dstPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
schemeSplit := strings.Split(config.Scheme, ":")
|
|
||||||
if len(schemeSplit) == 2 {
|
|
||||||
srcScheme = schemeSplit[0]
|
|
||||||
dstScheme = schemeSplit[1]
|
|
||||||
} else {
|
|
||||||
srcScheme = config.Scheme
|
|
||||||
dstScheme = config.Scheme
|
|
||||||
}
|
|
||||||
|
|
||||||
if srcScheme != dstScheme {
|
|
||||||
return nil, NewNestedError("unsupported").Subjectf("%v -> %v", srcScheme, dstScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &StreamRouteBase{
|
|
||||||
Alias: config.Alias,
|
|
||||||
Type: streamType,
|
|
||||||
ListeningScheme: srcScheme,
|
|
||||||
ListeningPort: srcPortInt,
|
|
||||||
TargetScheme: dstScheme,
|
|
||||||
TargetHost: config.Host,
|
|
||||||
TargetPort: dstPortInt,
|
|
||||||
|
|
||||||
id: config.GetID(),
|
|
||||||
wg: sync.WaitGroup{},
|
|
||||||
stopCh: make(chan struct{}, 1),
|
|
||||||
connCh: make(chan interface{}),
|
|
||||||
started: false,
|
|
||||||
l: l,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStreamRoute(config *ProxyConfig) (StreamRoute, error) {
|
|
||||||
base, err := newStreamRouteBase(config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch config.Scheme {
|
|
||||||
case StreamType_TCP:
|
|
||||||
base.StreamImpl = NewTCPRoute(base)
|
|
||||||
case StreamType_UDP:
|
|
||||||
base.StreamImpl = NewUDPRoute(base)
|
|
||||||
default:
|
|
||||||
return nil, NewNestedError("invalid stream type").Subject(config.Scheme)
|
|
||||||
}
|
|
||||||
return base, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (route *StreamRouteBase) ListeningUrl() string {
|
|
||||||
return fmt.Sprintf("%s:%v", route.ListeningScheme, route.ListeningPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (route *StreamRouteBase) TargetUrl() string {
|
|
||||||
return fmt.Sprintf("%s://%s:%v", route.TargetScheme, route.TargetHost, route.TargetPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (route *StreamRouteBase) Logger() logrus.FieldLogger {
|
|
||||||
return route.l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (route *StreamRouteBase) Start() {
|
|
||||||
route.wg.Wait()
|
|
||||||
route.ensurePort()
|
|
||||||
if err := route.Setup(); err != nil {
|
|
||||||
route.l.Errorf("failed to setup: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
route.started = true
|
|
||||||
streamRoutes.Set(route.id, route)
|
|
||||||
route.wg.Add(2)
|
|
||||||
go route.grAcceptConnections()
|
|
||||||
go route.grHandleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (route *StreamRouteBase) Stop() {
|
|
||||||
if !route.started {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
l := route.Logger()
|
|
||||||
l.Debug("stopping listening")
|
|
||||||
close(route.stopCh)
|
|
||||||
route.CloseListeners()
|
|
||||||
|
|
||||||
done := make(chan struct{}, 1)
|
|
||||||
go func() {
|
|
||||||
route.wg.Wait()
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
l.Info("stopped listening")
|
|
||||||
case <-time.After(streamStopListenTimeout):
|
|
||||||
l.Error("timed out waiting for connections")
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.unmarkPortInUse(route.ListeningPort)
|
|
||||||
streamRoutes.Delete(route.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (route *StreamRouteBase) ensurePort() {
|
|
||||||
if route.ListeningPort == 0 {
|
|
||||||
freePort, err := utils.findUseFreePort(20000)
|
|
||||||
if err != nil {
|
|
||||||
route.l.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
route.ListeningPort = freePort
|
|
||||||
route.l.Info("listening on free port ", route.ListeningPort)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
route.l.Info("listening on ", route.ListeningUrl())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (route *StreamRouteBase) grAcceptConnections() {
|
|
||||||
defer route.wg.Done()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-route.stopCh:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
conn, err := route.Accept()
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-route.stopCh:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
route.l.Error(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
route.connCh <- conn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (route *StreamRouteBase) grHandleConnections() {
|
|
||||||
defer route.wg.Done()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-route.stopCh:
|
|
||||||
return
|
|
||||||
case conn := <-route.connCh:
|
|
||||||
go func() {
|
|
||||||
err := route.Handle(conn)
|
|
||||||
if err != nil {
|
|
||||||
route.l.Error(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// id -> target
|
|
||||||
type StreamRoutes SafeMap[string, StreamRoute]
|
|
||||||
|
|
||||||
var streamRoutes StreamRoutes = NewSafeMapOf[StreamRoutes]()
|
|
|
@ -1,249 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/santhosh-tekuri/jsonschema"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
xhtml "golang.org/x/net/html"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Utils struct {
|
|
||||||
portsInUse map[int]bool
|
|
||||||
portsInUseMutex sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
var utils = &Utils{
|
|
||||||
portsInUse: make(map[int]bool),
|
|
||||||
portsInUseMutex: sync.Mutex{},
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Utils) findUseFreePort(startingPort int) (int, error) {
|
|
||||||
u.portsInUseMutex.Lock()
|
|
||||||
defer u.portsInUseMutex.Unlock()
|
|
||||||
for port := startingPort; port <= startingPort+100 && port <= 65535; port++ {
|
|
||||||
if u.portsInUse[port] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addr := fmt.Sprintf(":%d", port)
|
|
||||||
l, err := net.Listen("tcp", addr)
|
|
||||||
if err == nil {
|
|
||||||
u.portsInUse[port] = true
|
|
||||||
l.Close()
|
|
||||||
return port, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
l, err := net.Listen("tcp", ":0")
|
|
||||||
if err == nil {
|
|
||||||
// NOTE: may not be after 20000
|
|
||||||
port := l.Addr().(*net.TCPAddr).Port
|
|
||||||
u.portsInUse[port] = true
|
|
||||||
l.Close()
|
|
||||||
return port, nil
|
|
||||||
}
|
|
||||||
return -1, NewNestedError("unable to find free port").With(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Utils) markPortInUse(port int) {
|
|
||||||
u.portsInUseMutex.Lock()
|
|
||||||
u.portsInUse[port] = true
|
|
||||||
u.portsInUseMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Utils) unmarkPortInUse(port int) {
|
|
||||||
u.portsInUseMutex.Lock()
|
|
||||||
delete(u.portsInUse, port)
|
|
||||||
u.portsInUseMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Utils) healthCheckHttp(targetUrl string) error {
|
|
||||||
// try HEAD first
|
|
||||||
// if HEAD is not allowed, try GET
|
|
||||||
resp, err := healthCheckHttpClient.Head(targetUrl)
|
|
||||||
if resp != nil {
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
if err != nil && resp != nil && resp.StatusCode == http.StatusMethodNotAllowed {
|
|
||||||
_, err = healthCheckHttpClient.Get(targetUrl)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Utils) healthCheckStream(scheme, host string) error {
|
|
||||||
conn, err := net.DialTimeout(scheme, host, streamDialTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
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, "-", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func tryAppendPathPrefixImpl(pOrig, pAppend string) string {
|
|
||||||
switch {
|
|
||||||
case strings.Contains(pOrig, "://"):
|
|
||||||
return pOrig
|
|
||||||
case pOrig == "", pOrig == "#", pOrig == "/":
|
|
||||||
return pAppend
|
|
||||||
case filepath.IsLocal(pOrig) && !strings.HasPrefix(pOrig, pAppend):
|
|
||||||
return path.Join(pAppend, pOrig)
|
|
||||||
default:
|
|
||||||
return pOrig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var tryAppendPathPrefix func(string, string) string
|
|
||||||
var _ = func() int {
|
|
||||||
if logLevel == logrus.DebugLevel {
|
|
||||||
tryAppendPathPrefix = func(s1, s2 string) string {
|
|
||||||
replaced := tryAppendPathPrefixImpl(s1, s2)
|
|
||||||
return replaced
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tryAppendPathPrefix = tryAppendPathPrefixImpl
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}()
|
|
||||||
|
|
||||||
func htmlNodesSubPath(n *xhtml.Node, p string) {
|
|
||||||
if n.Type == xhtml.ElementNode {
|
|
||||||
for i, attr := range n.Attr {
|
|
||||||
switch attr.Key {
|
|
||||||
case "src", "href", "action": // img, script, link, form etc.
|
|
||||||
n.Attr[i].Val = tryAppendPathPrefix(attr.Val, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
||||||
htmlNodesSubPath(c, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Utils) respHTMLSubPath(r *http.Response, p string) error {
|
|
||||||
// remove all path prefix from relative path in script, img, a, ...
|
|
||||||
doc, err := xhtml.Parse(r.Body)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if p[0] == '/' {
|
|
||||||
p = p[1:]
|
|
||||||
}
|
|
||||||
htmlNodesSubPath(doc, p)
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err = xhtml.Render(&buf, doc)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Body = io.NopCloser(strings.NewReader(buf.String()))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Utils) respJSSubPath(r *http.Response, p string) error {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
_, err := buf.ReadFrom(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if p[0] == '/' {
|
|
||||||
p = p[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
js := buf.String()
|
|
||||||
|
|
||||||
re := regexp.MustCompile(`fetch\(["'].+["']\)`)
|
|
||||||
replace := func(match string) string {
|
|
||||||
match = match[7 : len(match)-2]
|
|
||||||
replaced := tryAppendPathPrefix(match, p)
|
|
||||||
return fmt.Sprintf(`fetch(%q)`, replaced)
|
|
||||||
}
|
|
||||||
js = re.ReplaceAllStringFunc(js, replace)
|
|
||||||
|
|
||||||
r.Body = io.NopCloser(strings.NewReader(js))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Utils) fileOK(path string) bool {
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setFieldFromSnake[T interface{}, VT interface{}](obj *T, field string, value VT) error {
|
|
||||||
field = utils.snakeToPascal(field)
|
|
||||||
prop := reflect.ValueOf(obj).Elem().FieldByName(field)
|
|
||||||
if prop.Kind() == 0 {
|
|
||||||
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)
|
|
||||||
if err != nil {
|
|
||||||
return NewNestedError("unable to unmarshal yaml").With(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := json.Marshal(i)
|
|
||||||
if err != nil {
|
|
||||||
return NewNestedError("unable to marshal json").With(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = schema.Validate(bytes.NewReader(m))
|
|
||||||
if err != nil {
|
|
||||||
valErr := err.(*jsonschema.ValidationError)
|
|
||||||
ne := NewNestedError("validation error")
|
|
||||||
for _, e := range valErr.Causes {
|
|
||||||
ne.ExtraError(e)
|
|
||||||
}
|
|
||||||
return ne
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,245 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/api/types/events"
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Watcher interface {
|
|
||||||
Start()
|
|
||||||
Stop()
|
|
||||||
Dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
type watcherBase struct {
|
|
||||||
onChange func()
|
|
||||||
l logrus.FieldLogger
|
|
||||||
sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
type fileWatcher struct {
|
|
||||||
*watcherBase
|
|
||||||
path string
|
|
||||||
onDelete func()
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerWatcher struct {
|
|
||||||
*watcherBase
|
|
||||||
client *client.Client
|
|
||||||
stopCh chan struct{}
|
|
||||||
wg sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) newWatcher() *watcherBase {
|
|
||||||
return &watcherBase{
|
|
||||||
onChange: p.ReloadRoutes,
|
|
||||||
l: p.l,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: 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()
|
|
||||||
if fsWatcher == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := fsWatcher.Add(w.path)
|
|
||||||
if err != nil {
|
|
||||||
w.l.Error("failed to start: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileWatchMap.Set(w.path, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *fileWatcher) Stop() {
|
|
||||||
w.Lock()
|
|
||||||
defer w.Unlock()
|
|
||||||
if fsWatcher == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileWatchMap.Delete(w.path)
|
|
||||||
err := fsWatcher.Remove(w.path)
|
|
||||||
if err != nil {
|
|
||||||
w.l.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *fileWatcher) Dispose() {
|
|
||||||
w.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *dockerWatcher) Start() {
|
|
||||||
w.Lock()
|
|
||||||
defer w.Unlock()
|
|
||||||
dockerWatchMap.Set(w.client.DaemonHost(), w)
|
|
||||||
w.wg.Add(1)
|
|
||||||
go w.watch()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *dockerWatcher) Stop() {
|
|
||||||
w.Lock()
|
|
||||||
defer w.Unlock()
|
|
||||||
if w.stopCh == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
close(w.stopCh)
|
|
||||||
w.wg.Wait()
|
|
||||||
w.stopCh = nil
|
|
||||||
dockerWatchMap.Delete(w.client.DaemonHost())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *dockerWatcher) Dispose() {
|
|
||||||
w.Stop()
|
|
||||||
w.client.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitFSWatcher() {
|
|
||||||
w, err := fsnotify.NewWatcher()
|
|
||||||
if err != nil {
|
|
||||||
wlog.Errorf("unable to create file watcher: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fsWatcher = w
|
|
||||||
fsWatcherWg.Add(1)
|
|
||||||
go watchFiles()
|
|
||||||
}
|
|
||||||
|
|
||||||
func StopFSWatcher() {
|
|
||||||
close(fsWatcherStop)
|
|
||||||
fsWatcherWg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func StopDockerWatcher() {
|
|
||||||
ParallelForEachValue(
|
|
||||||
dockerWatchMap.Iterator(),
|
|
||||||
(*dockerWatcher).Dispose,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func watchFiles() {
|
|
||||||
defer fsWatcher.Close()
|
|
||||||
defer fsWatcherWg.Done()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-fsWatcherStop:
|
|
||||||
return
|
|
||||||
case event, ok := <-fsWatcher.Events:
|
|
||||||
if !ok {
|
|
||||||
wlog.Error("file watcher channel closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w, ok := fileWatchMap.UnsafeGet(event.Name)
|
|
||||||
if !ok {
|
|
||||||
wlog.Errorf("watcher for %s not found", event.Name)
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case event.Has(fsnotify.Write):
|
|
||||||
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: ", event.Name)
|
|
||||||
go w.onDelete()
|
|
||||||
}
|
|
||||||
case err := <-fsWatcher.Errors:
|
|
||||||
wlog.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *dockerWatcher) watch() {
|
|
||||||
defer w.wg.Done()
|
|
||||||
|
|
||||||
filter := filters.NewArgs(
|
|
||||||
filters.Arg("type", "container"),
|
|
||||||
filters.Arg("event", "start"),
|
|
||||||
filters.Arg("event", "die"), // 'stop' already triggering 'die'
|
|
||||||
)
|
|
||||||
listen := func() (<-chan events.Message, <-chan error) {
|
|
||||||
return w.client.Events(context.Background(), types.EventsOptions{Filters: filter})
|
|
||||||
}
|
|
||||||
msgChan, errChan := listen()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-w.stopCh:
|
|
||||||
return
|
|
||||||
case msg := <-msgChan:
|
|
||||||
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("watcher: connection failed")
|
|
||||||
case client.IsErrNotFound(err):
|
|
||||||
w.l.Error("watcher: endpoint not found")
|
|
||||||
default:
|
|
||||||
w.l.Errorf("watcher: %v", err)
|
|
||||||
}
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
msgChan, errChan = listen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
FileWatcherMap = SafeMap[string, *fileWatcher]
|
|
||||||
DockerWatcherMap = SafeMap[string, *dockerWatcher]
|
|
||||||
)
|
|
||||||
|
|
||||||
var fsWatcher *fsnotify.Watcher
|
|
||||||
var (
|
|
||||||
fileWatchMap FileWatcherMap = NewSafeMapOf[FileWatcherMap]()
|
|
||||||
dockerWatchMap DockerWatcherMap = NewSafeMapOf[DockerWatcherMap]()
|
|
||||||
)
|
|
||||||
var (
|
|
||||||
fsWatcherStop = make(chan struct{}, 1)
|
|
||||||
)
|
|
||||||
var (
|
|
||||||
fsWatcherWg sync.WaitGroup
|
|
||||||
)
|
|
40
go.mod → src/go.mod
Executable file → Normal file
40
go.mod → src/go.mod
Executable file → Normal file
|
@ -3,52 +3,52 @@ module github.com/yusing/go-proxy
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/docker/cli v26.0.0+incompatible
|
github.com/docker/cli v27.1.1+incompatible
|
||||||
github.com/docker/docker v26.0.0+incompatible
|
github.com/docker/docker v27.1.1+incompatible
|
||||||
github.com/fsnotify/fsnotify v1.7.0
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
github.com/go-acme/lego/v4 v4.16.1
|
github.com/go-acme/lego/v4 v4.17.4
|
||||||
github.com/santhosh-tekuri/jsonschema v1.2.4
|
github.com/santhosh-tekuri/jsonschema v1.2.4
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
golang.org/x/net v0.24.0
|
golang.org/x/net v0.27.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cloudflare/cloudflare-go v0.92.0 // indirect
|
github.com/cloudflare/cloudflare-go v0.100.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.1 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.3 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||||
github.com/miekg/dns v1.1.58 // indirect
|
github.com/miekg/dns v1.1.61 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/term v0.5.0 // indirect
|
github.com/moby/term v0.5.0 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.25.0 // indirect
|
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.25.0 // indirect
|
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.25.0 // indirect
|
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||||
golang.org/x/crypto v0.22.0 // indirect
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
golang.org/x/mod v0.17.0 // indirect
|
golang.org/x/mod v0.19.0 // indirect
|
||||||
golang.org/x/sync v0.7.0 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.19.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.16.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
golang.org/x/tools v0.20.0 // indirect
|
golang.org/x/tools v0.23.0 // indirect
|
||||||
gotest.tools/v3 v3.5.1 // indirect
|
gotest.tools/v3 v3.5.1 // indirect
|
||||||
)
|
)
|
114
go.sum → src/go.sum
Executable file → Normal file
114
go.sum → src/go.sum
Executable file → Normal file
|
@ -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 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
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/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.100.0 h1:4iCUI2ZoIhRMyd7Z1TDsHhH1OhkgHC83eYbPlSgTRjo=
|
||||||
github.com/cloudflare/cloudflare-go v0.92.0/go.mod h1:nUqvBUUDRxNzsDSQjbqUNWHEIYAoUlgRmcAzMKlFdKs=
|
github.com/cloudflare/cloudflare-go v0.100.0/go.mod h1:VQ1t9Mvgdu4VFLx6uwQgFC10XxcCRIUuvkYGc9daMRU=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
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=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
@ -13,35 +13,33 @@ 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
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/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/cli v26.0.0+incompatible h1:90BKrx1a1HKYpSnnBFR6AgDq/FqkHxwlUyzJVPxD30I=
|
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE=
|
||||||
github.com/docker/cli v26.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/docker v26.0.0+incompatible h1:Ng2qi+gdKADUa/VM+6b6YaY2nlZhk/lVJiKR/2bMudU=
|
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
|
||||||
github.com/docker/docker v26.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/go-acme/lego/v4 v4.16.1 h1:JxZ93s4KG0jL27rZ30UsIgxap6VGzKuREsSkkyzeoCQ=
|
github.com/go-acme/lego/v4 v4.17.4 h1:h0nePd3ObP6o7kAkndtpTzCw8shOZuWckNYeUQwo36Q=
|
||||||
github.com/go-acme/lego/v4 v4.16.1/go.mod h1:AVvwdPned/IWpD/ihHhMsKnveF7HHYAz/CmtXi7OZoE=
|
github.com/go-acme/lego/v4 v4.17.4/go.mod h1:dU94SvPNqimEeb7EVilGGSnS0nU1O5Exir0pQ4QFL4U=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
|
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
@ -51,23 +49,22 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||||
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
|
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
|
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||||
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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||||
|
@ -89,43 +86,42 @@ github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHi
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
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.0/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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
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/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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 h1:cEPbyTSEHlQR89XVlyo78gqluF8Y3oMeBkXGWzQsfXY=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||||
go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
|
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||||
go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
|
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
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 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 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
||||||
go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
|
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||||
go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
|
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
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/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||||
go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
|
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||||
go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
|
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||||
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
|
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
|
||||||
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
|
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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
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.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.19.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-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-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-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.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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-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.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
@ -135,33 +131,33 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.22.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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
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-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.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
|
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||||
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
|
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao=
|
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||||
google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
|
google.golang.org/grpc v1.63.1 h1:pNClQmvdlyNUiwFETOux/PYqfhmA7BrswEdGRnib1fA=
|
||||||
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
|
google.golang.org/grpc v1.63.1/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
142
src/main.go
Executable file
142
src/main.go
Executable file
|
@ -0,0 +1,142 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/yusing/go-proxy/api"
|
||||||
|
apiUtils "github.com/yusing/go-proxy/api/v1/utils"
|
||||||
|
"github.com/yusing/go-proxy/common"
|
||||||
|
"github.com/yusing/go-proxy/config"
|
||||||
|
"github.com/yusing/go-proxy/docker"
|
||||||
|
R "github.com/yusing/go-proxy/route"
|
||||||
|
"github.com/yusing/go-proxy/server"
|
||||||
|
F "github.com/yusing/go-proxy/utils/functional"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
|
||||||
|
args := common.GetArgs()
|
||||||
|
l := logrus.WithField("?", "init")
|
||||||
|
|
||||||
|
if common.IsDebug {
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if common.IsRunningAsService {
|
||||||
|
logrus.SetFormatter(&logrus.TextFormatter{
|
||||||
|
DisableColors: true,
|
||||||
|
DisableTimestamp: true,
|
||||||
|
DisableSorting: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logrus.SetFormatter(&logrus.TextFormatter{
|
||||||
|
DisableSorting: true,
|
||||||
|
FullTimestamp: true,
|
||||||
|
TimestampFormat: "01-02 15:04:05",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Command == common.CommandReload {
|
||||||
|
if err := apiUtils.ReloadServer(); err.IsNotNil() {
|
||||||
|
l.Fatal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onShutdown := F.NewSlice[func()]()
|
||||||
|
cfg, err := config.New()
|
||||||
|
if err.IsNotNil() {
|
||||||
|
l.Fatalf("config error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// exit if only validate config
|
||||||
|
// TODO: validate without load
|
||||||
|
if args.Command == common.CommandValidate {
|
||||||
|
l.Printf("config OK")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onShutdown.Add(func() {
|
||||||
|
docker.CloseAllClients()
|
||||||
|
cfg.Dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGINT)
|
||||||
|
signal.Notify(sig, syscall.SIGTERM)
|
||||||
|
signal.Notify(sig, syscall.SIGHUP)
|
||||||
|
|
||||||
|
autocert := cfg.GetAutoCertProvider()
|
||||||
|
err = autocert.LoadCert()
|
||||||
|
|
||||||
|
if err.IsNotNil() {
|
||||||
|
l.Infof("error loading certificate: %s\nNow attempting to obtain a new certificate", err)
|
||||||
|
if err = autocert.ObtainCert(); err.IsNotNil() {
|
||||||
|
ctx, certRenewalCancel := context.WithCancel(context.Background())
|
||||||
|
go autocert.ScheduleRenewal(ctx)
|
||||||
|
onShutdown.Add(certRenewalCancel)
|
||||||
|
} else {
|
||||||
|
l.Warn(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for name, expiry := range autocert.GetExpiries() {
|
||||||
|
l.Infof("certificate %q: expire on %s", name, expiry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxyServer := server.InitProxyServer(server.Options{
|
||||||
|
Name: "proxy",
|
||||||
|
CertProvider: autocert,
|
||||||
|
HTTPPort: common.ProxyHTTPPort,
|
||||||
|
HTTPSPort: common.ProxyHTTPSPort,
|
||||||
|
Handler: http.HandlerFunc(R.ProxyHandler),
|
||||||
|
RedirectToHTTPS: cfg.Value().RedirectToHTTPS,
|
||||||
|
})
|
||||||
|
apiServer := server.InitAPIServer(server.Options{
|
||||||
|
Name: "api",
|
||||||
|
CertProvider: autocert,
|
||||||
|
HTTPPort: common.APIHTTPPort,
|
||||||
|
Handler: api.NewHandler(cfg),
|
||||||
|
RedirectToHTTPS: cfg.Value().RedirectToHTTPS,
|
||||||
|
})
|
||||||
|
|
||||||
|
proxyServer.Start()
|
||||||
|
apiServer.Start()
|
||||||
|
onShutdown.Add(proxyServer.Stop)
|
||||||
|
onShutdown.Add(apiServer.Stop)
|
||||||
|
|
||||||
|
// wait for signal
|
||||||
|
<-sig
|
||||||
|
|
||||||
|
// grafully shutdown
|
||||||
|
logrus.Info("shutting down")
|
||||||
|
done := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(onShutdown.Size())
|
||||||
|
onShutdown.ForEach(func(f func()) {
|
||||||
|
go func() {
|
||||||
|
f()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
logrus.Info("shutdown complete")
|
||||||
|
case <-time.After(time.Duration(cfg.Value().TimeoutShutdown) * time.Second):
|
||||||
|
logrus.Info("timeout waiting for shutdown")
|
||||||
|
}
|
||||||
|
}
|
13
src/models/autocert_config.go
Normal file
13
src/models/autocert_config.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type (
|
||||||
|
AutoCertConfig struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Domains []string `yaml:",flow" json:"domains"`
|
||||||
|
CertPath string `yaml:"cert_path" json:"cert_path"`
|
||||||
|
KeyPath string `yaml:"key_path" json:"key_path"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Options AutocertProviderOpt `yaml:",flow" json:"options"`
|
||||||
|
}
|
||||||
|
AutocertProviderOpt map[string]string
|
||||||
|
)
|
16
src/models/config.go
Normal file
16
src/models/config.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Providers ProxyProviders `yaml:",flow" json:"providers"`
|
||||||
|
AutoCert AutoCertConfig `yaml:",flow" json:"autocert"`
|
||||||
|
TimeoutShutdown int `yaml:"timeout_shutdown" json:"timeout_shutdown"`
|
||||||
|
RedirectToHTTPS bool `yaml:"redirect_to_https" json:"redirect_to_https"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Providers: ProxyProviders{},
|
||||||
|
TimeoutShutdown: 3,
|
||||||
|
RedirectToHTTPS: true,
|
||||||
|
}
|
||||||
|
}
|
43
src/models/proxy_entry.go
Normal file
43
src/models/proxy_entry.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
F "github.com/yusing/go-proxy/utils/functional"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ProxyEntry struct {
|
||||||
|
Alias string `yaml:"-" json:"-"`
|
||||||
|
Scheme string `yaml:"scheme" json:"scheme"`
|
||||||
|
Host string `yaml:"host" json:"host"`
|
||||||
|
Port string `yaml:"port" json:"port"`
|
||||||
|
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // http proxy only
|
||||||
|
Path string `yaml:"path" json:"path"` // 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
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxyEntries = *F.Map[string, *ProxyEntry]
|
||||||
|
)
|
||||||
|
|
||||||
|
var NewProxyEntries = F.NewMap[string, *ProxyEntry]
|
||||||
|
|
||||||
|
func (e *ProxyEntry) SetDefaults() {
|
||||||
|
if e.Scheme == "" {
|
||||||
|
if strings.ContainsRune(e.Port, ':') {
|
||||||
|
e.Scheme = "tcp"
|
||||||
|
} else {
|
||||||
|
switch e.Port {
|
||||||
|
case "443", "8443":
|
||||||
|
e.Scheme = "https"
|
||||||
|
default:
|
||||||
|
e.Scheme = "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.Path == "" {
|
||||||
|
e.Path = "/"
|
||||||
|
}
|
||||||
|
}
|
9
src/models/proxy_provider.go
Normal file
9
src/models/proxy_provider.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type (
|
||||||
|
ProxyProvider struct {
|
||||||
|
Kind string `json:"kind"` // docker, file
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
ProxyProviders = map[string]ProxyProvider
|
||||||
|
)
|
22
src/proxy/constants.go
Normal file
22
src/proxy/constants.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
var (
|
||||||
|
PathMode_Forward = "forward"
|
||||||
|
PathMode_RemovedPath = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StreamType_UDP string = "udp"
|
||||||
|
StreamType_TCP string = "tcp"
|
||||||
|
// StreamType_UDP_TCP Scheme = "udp-tcp"
|
||||||
|
// StreamType_TCP_UDP Scheme = "tcp-udp"
|
||||||
|
// StreamType_TLS Scheme = "tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// TODO: support "tcp-udp", "udp-tcp", etc.
|
||||||
|
StreamSchemes = []string{StreamType_TCP, StreamType_UDP}
|
||||||
|
HTTPSchemes = []string{"http", "https"}
|
||||||
|
ValidSchemes = append(StreamSchemes, HTTPSchemes...)
|
||||||
|
)
|
||||||
|
|
94
src/proxy/entry.go
Normal file
94
src/proxy/entry.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
M "github.com/yusing/go-proxy/models"
|
||||||
|
T "github.com/yusing/go-proxy/proxy/fields"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Entry struct { // real model after validation
|
||||||
|
Alias T.Alias
|
||||||
|
Scheme T.Scheme
|
||||||
|
Host T.Host
|
||||||
|
Port T.Port
|
||||||
|
URL *url.URL
|
||||||
|
NoTLSVerify bool
|
||||||
|
Path T.Path
|
||||||
|
SetHeaders http.Header
|
||||||
|
HideHeaders []string
|
||||||
|
}
|
||||||
|
StreamEntry struct {
|
||||||
|
Alias T.Alias `json:"alias"`
|
||||||
|
Scheme T.StreamScheme `json:"scheme"`
|
||||||
|
Host T.Host `json:"host"`
|
||||||
|
Port T.StreamPort `json:"port"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewEntry(m *M.ProxyEntry) (any, E.NestedError) {
|
||||||
|
m.SetDefaults()
|
||||||
|
scheme, err := T.NewScheme(m.Scheme)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if scheme.IsStream() {
|
||||||
|
return validateStreamEntry(m)
|
||||||
|
}
|
||||||
|
return validateEntry(m, *scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateEntry(m *M.ProxyEntry, s T.Scheme) (*Entry, E.NestedError) {
|
||||||
|
host, err := T.NewHost(m.Host)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
port, err := T.NewPort(m.Port)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
path, err := T.NewPath(m.Path)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
url, err := E.Check(url.Parse(s.String() + "://" + host.String() + ":" + strconv.Itoa(int(port))))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Entry{
|
||||||
|
Alias: T.NewAlias(m.Alias),
|
||||||
|
Scheme: s,
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
URL: url,
|
||||||
|
NoTLSVerify: m.NoTLSVerify,
|
||||||
|
Path: path,
|
||||||
|
SetHeaders: m.SetHeaders,
|
||||||
|
HideHeaders: m.HideHeaders,
|
||||||
|
}, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateStreamEntry(m *M.ProxyEntry) (*StreamEntry, E.NestedError) {
|
||||||
|
host, err := T.NewHost(m.Host)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
port, err := T.NewStreamPort(m.Port)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scheme, err := T.NewStreamScheme(m.Scheme)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &StreamEntry{
|
||||||
|
Alias: T.NewAlias(m.Alias),
|
||||||
|
Scheme: *scheme,
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
}, E.Nil()
|
||||||
|
}
|
23
src/proxy/fields/alias.go
Normal file
23
src/proxy/fields/alias.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
F "github.com/yusing/go-proxy/utils/functional"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Alias struct{ F.Stringable }
|
||||||
|
type Aliases struct{ *F.Slice[Alias] }
|
||||||
|
|
||||||
|
func NewAlias(s string) Alias {
|
||||||
|
return Alias{F.NewStringable(s)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAliases(s string) Aliases {
|
||||||
|
split := strings.Split(s, ",")
|
||||||
|
a := Aliases{F.NewSliceN[Alias](len(split))}
|
||||||
|
for i, v := range split {
|
||||||
|
a.Set(i, NewAlias(v))
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
20
src/proxy/fields/host.go
Normal file
20
src/proxy/fields/host.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
F "github.com/yusing/go-proxy/utils/functional"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Host struct{ F.Stringable }
|
||||||
|
type Subdomain = Alias
|
||||||
|
|
||||||
|
func NewHost(s string) (Host, E.NestedError) {
|
||||||
|
return Host{F.NewStringable(s)}, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Host) Subdomain() (*Subdomain, E.NestedError) {
|
||||||
|
if i := h.IndexRune(':'); i != -1 {
|
||||||
|
return &Subdomain{h.SubStr(0, i)}, E.Nil()
|
||||||
|
}
|
||||||
|
return nil, E.Invalid("host", h)
|
||||||
|
}
|
15
src/proxy/fields/path.go
Normal file
15
src/proxy/fields/path.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
F "github.com/yusing/go-proxy/utils/functional"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Path struct{ F.Stringable }
|
||||||
|
|
||||||
|
func NewPath(s string) (Path, E.NestedError) {
|
||||||
|
if s == "" || s[0] == '/' {
|
||||||
|
return Path{F.NewStringable(s)}, E.Nil()
|
||||||
|
}
|
||||||
|
return Path{}, E.Invalid("path", s).Extra("must be empty or start with '/'")
|
||||||
|
}
|
25
src/proxy/fields/path_mode.go
Normal file
25
src/proxy/fields/path_mode.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
F "github.com/yusing/go-proxy/utils/functional"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PathMode struct{ F.Stringable }
|
||||||
|
|
||||||
|
func NewPathMode(pm string) (PathMode, E.NestedError) {
|
||||||
|
switch pm {
|
||||||
|
case "", "forward":
|
||||||
|
return PathMode{F.NewStringable(pm)}, E.Nil()
|
||||||
|
default:
|
||||||
|
return PathMode{}, E.Invalid("path mode", pm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PathMode) IsRemove() bool {
|
||||||
|
return p.String() == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PathMode) IsForward() bool {
|
||||||
|
return p.String() == "forward"
|
||||||
|
}
|
40
src/proxy/fields/port.go
Normal file
40
src/proxy/fields/port.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Port int
|
||||||
|
|
||||||
|
func NewPort(v string) (Port, E.NestedError) {
|
||||||
|
p, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
return ErrPort, E.From(err)
|
||||||
|
}
|
||||||
|
return NewPortInt(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPortInt(v int) (Port, E.NestedError) {
|
||||||
|
pp := Port(v)
|
||||||
|
if err := pp.boundCheck(); err.IsNotNil() {
|
||||||
|
return ErrPort, err
|
||||||
|
}
|
||||||
|
return pp, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Port) boundCheck() E.NestedError {
|
||||||
|
if p < MinPort || p > MaxPort {
|
||||||
|
return E.Invalid("port", p)
|
||||||
|
}
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
MinPort = 0
|
||||||
|
MaxPort = 65535
|
||||||
|
ErrPort = Port(-1)
|
||||||
|
NoPort = Port(-1)
|
||||||
|
ZeroPort = Port(0)
|
||||||
|
)
|
37
src/proxy/fields/scheme.go
Normal file
37
src/proxy/fields/scheme.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
F "github.com/yusing/go-proxy/utils/functional"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scheme struct{ F.Stringable }
|
||||||
|
|
||||||
|
func NewScheme(s string) (*Scheme, E.NestedError) {
|
||||||
|
switch s {
|
||||||
|
case "http", "https", "tcp", "udp":
|
||||||
|
return &Scheme{F.NewStringable(s)}, E.Nil()
|
||||||
|
}
|
||||||
|
return nil, E.Invalid("scheme", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSchemeFromPort(p string) (*Scheme, E.NestedError) {
|
||||||
|
var s string
|
||||||
|
switch {
|
||||||
|
case strings.ContainsRune(p, ':'):
|
||||||
|
s = "tcp"
|
||||||
|
case strings.HasSuffix(p, "443"):
|
||||||
|
s = "https"
|
||||||
|
default:
|
||||||
|
s = "http"
|
||||||
|
}
|
||||||
|
return &Scheme{F.NewStringable(s)}, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Scheme) IsHTTP() bool { return s.String() == "http" }
|
||||||
|
func (s Scheme) IsHTTPS() bool { return s.String() == "https" }
|
||||||
|
func (s Scheme) IsTCP() bool { return s.String() == "tcp" }
|
||||||
|
func (s Scheme) IsUDP() bool { return s.String() == "udp" }
|
||||||
|
func (s Scheme) IsStream() bool { return s.IsTCP() || s.IsUDP() }
|
49
src/proxy/fields/stream_port.go
Normal file
49
src/proxy/fields/stream_port.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/common"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StreamPort struct {
|
||||||
|
ListeningPort Port `json:"listening"`
|
||||||
|
ProxyPort Port `json:"proxy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStreamPort(p string) (StreamPort, E.NestedError) {
|
||||||
|
split := strings.Split(p, ":")
|
||||||
|
if len(split) != 2 {
|
||||||
|
return StreamPort{}, E.Invalid("stream port", p).Extra("should be in 'x:y' format")
|
||||||
|
}
|
||||||
|
|
||||||
|
listeningPort, err := NewPort(split[0])
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return StreamPort{}, err
|
||||||
|
}
|
||||||
|
if err = listeningPort.boundCheck(); err.IsNotNil() {
|
||||||
|
return StreamPort{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyPort, err := NewPort(split[1])
|
||||||
|
if err.IsNotNil() {
|
||||||
|
proxyPort, err = parseNameToPort(split[1])
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return StreamPort{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = proxyPort.boundCheck(); err.IsNotNil() {
|
||||||
|
return StreamPort{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return StreamPort{ListeningPort: listeningPort, ProxyPort: proxyPort}, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNameToPort(name string) (Port, E.NestedError) {
|
||||||
|
port, ok := common.NamePortMapTCP[name]
|
||||||
|
if !ok {
|
||||||
|
return -1, E.Unsupported("service", name)
|
||||||
|
}
|
||||||
|
return Port(port), E.Nil()
|
||||||
|
}
|
42
src/proxy/fields/stream_scheme.go
Normal file
42
src/proxy/fields/stream_scheme.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StreamScheme struct {
|
||||||
|
ListeningScheme *Scheme `json:"listening"`
|
||||||
|
ProxyScheme *Scheme `json:"proxy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStreamScheme(s string) (ss *StreamScheme, err E.NestedError) {
|
||||||
|
ss = &StreamScheme{}
|
||||||
|
parts := strings.Split(s, ":")
|
||||||
|
if len(parts) == 1 {
|
||||||
|
parts = []string{s, s}
|
||||||
|
} else if len(parts) != 2 {
|
||||||
|
return nil, E.Invalid("stream scheme", s)
|
||||||
|
}
|
||||||
|
ss.ListeningScheme, err = NewScheme(parts[0])
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ss.ProxyScheme, err = NewScheme(parts[1])
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ss, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s StreamScheme) String() string {
|
||||||
|
return s.ListeningScheme.String() + " -> " + s.ProxyScheme.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCoherent checks if the ListeningScheme and ProxyScheme of the StreamScheme are equal.
|
||||||
|
//
|
||||||
|
// It returns a boolean value indicating whether the ListeningScheme and ProxyScheme are equal.
|
||||||
|
func (s StreamScheme) IsCoherent() bool {
|
||||||
|
return *s.ListeningScheme == *s.ProxyScheme
|
||||||
|
}
|
3
src/proxy/provider/constants.go
Normal file
3
src/proxy/provider/constants.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
const wildcardAlias = "*"
|
149
src/proxy/provider/docker_provider.go
Executable file
149
src/proxy/provider/docker_provider.go
Executable file
|
@ -0,0 +1,149 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
D "github.com/yusing/go-proxy/docker"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
M "github.com/yusing/go-proxy/models"
|
||||||
|
PT "github.com/yusing/go-proxy/proxy/fields"
|
||||||
|
W "github.com/yusing/go-proxy/watcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DockerProvider struct {
|
||||||
|
dockerHost string
|
||||||
|
}
|
||||||
|
|
||||||
|
func DockerProviderImpl(model *M.ProxyProvider) ProviderImpl {
|
||||||
|
return &DockerProvider{dockerHost: model.Value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProxyEntries returns proxy entries from a docker client.
|
||||||
|
//
|
||||||
|
// It retrieves the docker client information using the dockerhelper.GetClientInfo method.
|
||||||
|
// Then, it iterates over the containers in the docker client information and calls
|
||||||
|
// the getEntriesFromLabels method to get the proxy entries for each container.
|
||||||
|
// Any errors encountered during the process are added to the ne error object.
|
||||||
|
// Finally, it returns the collected proxy entries and the ne error object.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - p: A pointer to the DockerProvider struct.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - P.EntryModelSlice: A slice of EntryModel structs representing the proxy entries.
|
||||||
|
// - error: An error object if there was an error retrieving the docker client information or parsing the labels.
|
||||||
|
func (p DockerProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) {
|
||||||
|
info, err := D.GetClientInfo(p.dockerHost)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, E.From(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := M.NewProxyEntries()
|
||||||
|
errors := E.NewBuilder("errors when parse docker labels for %q", p.dockerHost)
|
||||||
|
|
||||||
|
for _, container := range info.Containers {
|
||||||
|
en, err := p.getEntriesFromLabels(&container, info.Host)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
errors.Add(err)
|
||||||
|
}
|
||||||
|
// although err is not nil
|
||||||
|
// there may be some valid entries in `en`
|
||||||
|
dups := entries.MergeWith(en)
|
||||||
|
// add the duplicate proxy entries to the error
|
||||||
|
dups.EachKV(func(k string, v *M.ProxyEntry) {
|
||||||
|
errors.Addf("duplicate alias %s", k)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries, errors.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DockerProvider) NewWatcher() W.Watcher {
|
||||||
|
return W.NewDockerWatcher(p.dockerHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a list of proxy entries for a container.
|
||||||
|
// Always non-nil
|
||||||
|
func (p *DockerProvider) getEntriesFromLabels(container *types.Container, clientHost string) (M.ProxyEntries, E.NestedError) {
|
||||||
|
var mainAlias string
|
||||||
|
var aliases PT.Aliases
|
||||||
|
|
||||||
|
// set mainAlias to docker compose service name if available
|
||||||
|
if serviceName, ok := container.Labels["com.docker.compose.service"]; ok {
|
||||||
|
mainAlias = serviceName
|
||||||
|
}
|
||||||
|
|
||||||
|
// if mainAlias is not set,
|
||||||
|
// or container name is different from service name
|
||||||
|
// use container name
|
||||||
|
if containerName := strings.TrimPrefix(container.Names[0], "/"); containerName != mainAlias {
|
||||||
|
mainAlias = containerName
|
||||||
|
}
|
||||||
|
|
||||||
|
if l, ok := container.Labels["proxy.aliases"]; ok {
|
||||||
|
aliases = PT.NewAliases(l)
|
||||||
|
delete(container.Labels, "proxy.aliases")
|
||||||
|
} else {
|
||||||
|
aliases = PT.NewAliases(mainAlias)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := M.NewProxyEntries()
|
||||||
|
|
||||||
|
// find first port, return if no port exposed
|
||||||
|
defaultPort := findFirstPort(container)
|
||||||
|
if defaultPort == PT.NoPort {
|
||||||
|
return entries, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
// init entries map for all aliases
|
||||||
|
aliases.ForEach(func(a PT.Alias) {
|
||||||
|
entries.Set(a.String(), &M.ProxyEntry{
|
||||||
|
Alias: a.String(),
|
||||||
|
Host: clientHost,
|
||||||
|
Port: fmt.Sprint(defaultPort),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
errors := E.NewBuilder("failed to apply label for %q", mainAlias)
|
||||||
|
for key, val := range container.Labels {
|
||||||
|
lbl, err := D.ParseLabel(key, val)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
errors.Add(E.From(err).Subject(key))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lbl.Namespace != D.NSProxy {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lbl.Target == wildcardAlias {
|
||||||
|
// apply label for all aliases
|
||||||
|
entries.EachKV(func(a string, e *M.ProxyEntry) {
|
||||||
|
if err = D.ApplyLabel(e, lbl); err.IsNotNil() {
|
||||||
|
errors.Add(E.From(err).Subject(lbl.Target))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
config, ok := entries.UnsafeGet(lbl.Target)
|
||||||
|
if !ok {
|
||||||
|
errors.Add(E.NotExists("alias", lbl.Target))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = D.ApplyLabel(config, lbl); err.IsNotNil() {
|
||||||
|
errors.Add(err.Subject(lbl.Target))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries, errors.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
func findFirstPort(c *types.Container) (pp PT.Port) {
|
||||||
|
for _, p := range c.Ports {
|
||||||
|
if p.PublicPort != 0 || c.HostConfig.NetworkMode == "host" {
|
||||||
|
pp, _ = PT.NewPortInt(int(p.PublicPort))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PT.NoPort
|
||||||
|
}
|
50
src/proxy/provider/file_provider.go
Normal file
50
src/proxy/provider/file_provider.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/common"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
M "github.com/yusing/go-proxy/models"
|
||||||
|
U "github.com/yusing/go-proxy/utils"
|
||||||
|
W "github.com/yusing/go-proxy/watcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileProvider struct {
|
||||||
|
fileName string
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileProviderImpl(m *M.ProxyProvider) ProviderImpl {
|
||||||
|
return &FileProvider{
|
||||||
|
fileName: m.Value,
|
||||||
|
path: path.Join(common.ConfigBasePath, m.Value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Validate(data []byte) E.NestedError {
|
||||||
|
return U.ValidateYaml(U.GetSchema(common.ProvidersSchemaPath), data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FileProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) {
|
||||||
|
entries := M.NewProxyEntries()
|
||||||
|
data, err := E.Check(os.ReadFile(p.path))
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return entries, E.Failure("read file").Subject(p.fileName).With(err)
|
||||||
|
}
|
||||||
|
ne := E.Failure("validation").Subject(p.fileName)
|
||||||
|
if !common.NoSchemaValidation {
|
||||||
|
if err = Validate(data); err.IsNotNil() {
|
||||||
|
return entries, ne.With(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = entries.UnmarshalFromYAML(data); err.IsNotNil() {
|
||||||
|
return entries, ne.With(err)
|
||||||
|
}
|
||||||
|
return entries, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FileProvider) NewWatcher() W.Watcher {
|
||||||
|
return W.NewFileWatcher(p.fileName)
|
||||||
|
}
|
165
src/proxy/provider/provider.go
Normal file
165
src/proxy/provider/provider.go
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/yusing/go-proxy/common"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
M "github.com/yusing/go-proxy/models"
|
||||||
|
R "github.com/yusing/go-proxy/route"
|
||||||
|
W "github.com/yusing/go-proxy/watcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProviderImpl interface {
|
||||||
|
GetProxyEntries() (M.ProxyEntries, E.NestedError)
|
||||||
|
NewWatcher() W.Watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
ProviderImpl
|
||||||
|
|
||||||
|
name string
|
||||||
|
routes *R.Routes
|
||||||
|
reloadReqCh chan struct{}
|
||||||
|
|
||||||
|
watcher W.Watcher
|
||||||
|
watcherCtx context.Context
|
||||||
|
watcherCancel context.CancelFunc
|
||||||
|
|
||||||
|
l *logrus.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvider(name string, model M.ProxyProvider) (p *Provider) {
|
||||||
|
p = &Provider{
|
||||||
|
name: name,
|
||||||
|
routes: R.NewRoutes(),
|
||||||
|
reloadReqCh: make(chan struct{}, 1),
|
||||||
|
l: logrus.WithField("provider", name),
|
||||||
|
}
|
||||||
|
switch model.Kind {
|
||||||
|
case common.ProviderKind_Docker:
|
||||||
|
p.ProviderImpl = DockerProviderImpl(&model)
|
||||||
|
case common.ProviderKind_File:
|
||||||
|
p.ProviderImpl = FileProviderImpl(&model)
|
||||||
|
}
|
||||||
|
p.watcher = p.NewWatcher()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) GetName() string {
|
||||||
|
return p.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) StartAllRoutes() E.NestedError {
|
||||||
|
err := p.loadRoutes()
|
||||||
|
|
||||||
|
// start watcher no matter load success or not
|
||||||
|
p.watcherCtx, p.watcherCancel = context.WithCancel(context.Background())
|
||||||
|
go p.watchEvents()
|
||||||
|
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
errors := E.NewBuilder("errors starting routes for provider %q", p.name)
|
||||||
|
nStarted := 0
|
||||||
|
p.routes.EachKVParallel(func(alias string, r R.Route) {
|
||||||
|
if err := r.Start(); err.IsNotNil() {
|
||||||
|
errors.Add(err.Subject(alias))
|
||||||
|
} else {
|
||||||
|
nStarted++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err := errors.Build(); err.IsNotNil() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.l.Infof("%d routes started", nStarted)
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) StopAllRoutes() E.NestedError {
|
||||||
|
defer p.routes.Clear()
|
||||||
|
|
||||||
|
if p.watcherCancel != nil {
|
||||||
|
p.watcherCancel()
|
||||||
|
}
|
||||||
|
errors := E.NewBuilder("errors stopping routes for provider %q", p.name)
|
||||||
|
nStopped := 0
|
||||||
|
p.routes.EachKVParallel(func(alias string, r R.Route) {
|
||||||
|
if err := r.Stop(); err.IsNotNil() {
|
||||||
|
errors.Add(err.Subject(alias))
|
||||||
|
} else {
|
||||||
|
nStopped++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err := errors.Build(); err.IsNotNil() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.l.Infof("%d routes stopped", nStopped)
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) ReloadRoutes() {
|
||||||
|
defer p.l.Info("routes reloaded")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case p.reloadReqCh <- struct{}{}:
|
||||||
|
defer func() {
|
||||||
|
<-p.reloadReqCh
|
||||||
|
}()
|
||||||
|
p.StopAllRoutes()
|
||||||
|
p.loadRoutes()
|
||||||
|
p.StartAllRoutes()
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) GetCurrentRoutes() *R.Routes {
|
||||||
|
return p.routes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) watchEvents() {
|
||||||
|
events, errs := p.watcher.Events(p.watcherCtx)
|
||||||
|
l := logrus.WithField("?", "watcher")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.reloadReqCh:
|
||||||
|
p.ReloadRoutes()
|
||||||
|
case event, ok := <-events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.Infof("watcher event: %v", event)
|
||||||
|
p.reloadReqCh <- struct{}{}
|
||||||
|
case err, ok := <-errs:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.Errorf("watcher error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) loadRoutes() E.NestedError {
|
||||||
|
entries, err := p.GetProxyEntries()
|
||||||
|
|
||||||
|
if err.IsNotNil() {
|
||||||
|
p.l.Warn(err.Subjectf("provider %s", p.name))
|
||||||
|
}
|
||||||
|
p.routes = R.NewRoutes()
|
||||||
|
|
||||||
|
errors := E.NewBuilder("errors loading routes from provider %q", p.name)
|
||||||
|
entries.EachKV(func(a string, e *M.ProxyEntry) {
|
||||||
|
r, err := R.NewRoute(e)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
errors.Addf("%s: %w", a, err)
|
||||||
|
p.l.Debugf("failed to load route: %s, %s", a, err)
|
||||||
|
} else {
|
||||||
|
p.routes.Set(a, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
p.l.Debugf("loaded %d routes from %d entries", p.routes.Size(), entries.Size())
|
||||||
|
return errors.Build()
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package proxy
|
||||||
|
|
||||||
// A small mod on net/http/httputil/reverseproxy.go
|
// A small mod on net/http/httputil/reverseproxy.go
|
||||||
// that doubled the performance
|
// that doubled the performance
|
||||||
|
@ -15,6 +15,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/http/httpguts"
|
"golang.org/x/net/http/httpguts"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,23 +32,6 @@ type ProxyRequest struct {
|
||||||
Out *http.Request
|
Out *http.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetURL routes the outbound request 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".
|
|
||||||
//
|
|
||||||
// SetURL rewrites the outbound Host header to match the target's host.
|
|
||||||
// To preserve the inbound request's Host header (the default behavior
|
|
||||||
// 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 = ""
|
|
||||||
// }
|
|
||||||
|
|
||||||
// SetXForwarded sets the X-Forwarded-For, X-Forwarded-Host, and
|
// SetXForwarded sets the X-Forwarded-For, X-Forwarded-Host, and
|
||||||
// X-Forwarded-Proto headers of the outbound request.
|
// X-Forwarded-Proto headers of the outbound request.
|
||||||
//
|
//
|
||||||
|
@ -221,13 +205,15 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
|
||||||
// r.Out.Host = r.In.Host // if desired
|
// r.Out.Host = r.In.Host // if desired
|
||||||
// },
|
// },
|
||||||
// }
|
// }
|
||||||
func NewReverseProxy(target *url.URL, transport *http.Transport, config *ProxyConfig) *ReverseProxy {
|
//
|
||||||
|
// TODO: headers in ModifyResponse
|
||||||
|
func NewReverseProxy(target *url.URL, transport *http.Transport, entry *Entry) *ReverseProxy {
|
||||||
// check on init rather than on request
|
// check on init rather than on request
|
||||||
var setHeaders = func(r *http.Request) {}
|
var setHeaders = func(r *http.Request) {}
|
||||||
var hideHeaders = func(r *http.Request) {}
|
var hideHeaders = func(r *http.Request) {}
|
||||||
if len(config.SetHeaders) > 0 {
|
if len(entry.SetHeaders) > 0 {
|
||||||
setHeaders = func(r *http.Request) {
|
setHeaders = func(r *http.Request) {
|
||||||
h := config.SetHeaders.Clone()
|
h := entry.SetHeaders.Clone()
|
||||||
for k, vv := range h {
|
for k, vv := range h {
|
||||||
if k == "Host" {
|
if k == "Host" {
|
||||||
r.Host = vv[0]
|
r.Host = vv[0]
|
||||||
|
@ -237,9 +223,9 @@ func NewReverseProxy(target *url.URL, transport *http.Transport, config *ProxyCo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(config.HideHeaders) > 0 {
|
if len(entry.HideHeaders) > 0 {
|
||||||
hideHeaders = func(r *http.Request) {
|
hideHeaders = func(r *http.Request) {
|
||||||
for _, k := range config.HideHeaders {
|
for _, k := range entry.HideHeaders {
|
||||||
r.Header.Del(k)
|
r.Header.Del(k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -272,27 +258,8 @@ func copyHeader(dst, src http.Header) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hop-by-hop headers. These are removed when sent to the backend.
|
|
||||||
// As of RFC 7230, hop-by-hop headers are required to appear in the
|
|
||||||
// Connection header field. These are the headers defined by the
|
|
||||||
// obsoleted RFC 2616 (section 13.5.1) and are used for backward
|
|
||||||
// compatibility.
|
|
||||||
// var hopHeaders = []string{
|
|
||||||
// "Connection",
|
|
||||||
// "Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
|
|
||||||
// "Keep-Alive",
|
|
||||||
// "Proxy-Authenticate",
|
|
||||||
// "Proxy-Authorization",
|
|
||||||
// "Te", // canonicalized version of "TE"
|
|
||||||
// "Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522
|
|
||||||
// "Transfer-Encoding",
|
|
||||||
// "Upgrade",
|
|
||||||
// }
|
|
||||||
|
|
||||||
// NOTE: getErrorHandler and DefaultErrorHandler removed
|
|
||||||
|
|
||||||
func (p *ReverseProxy) errorHandler(rw http.ResponseWriter, _ *http.Request, err error) {
|
func (p *ReverseProxy) errorHandler(rw http.ResponseWriter, _ *http.Request, err error) {
|
||||||
p.logf("http: proxy error: %v", err)
|
logger.Errorf("http: proxy error: %s", err)
|
||||||
rw.WriteHeader(http.StatusBadGateway)
|
rw.WriteHeader(http.StatusBadGateway)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,10 +279,6 @@ func (p *ReverseProxy) modifyResponse(rw http.ResponseWriter, res *http.Response
|
||||||
|
|
||||||
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
transport := p.Transport
|
transport := p.Transport
|
||||||
// Note: removed
|
|
||||||
// if transport == nil {
|
|
||||||
// transport = http.DefaultTransport
|
|
||||||
// }
|
|
||||||
|
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
if ctx.Done() != nil {
|
if ctx.Done() != nil {
|
||||||
|
@ -360,18 +323,6 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
outreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate
|
outreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: removed
|
|
||||||
// if (p.Director != nil) == (p.Rewrite != nil) {
|
|
||||||
// p.errorHandler(rw, req, errors.New("ReverseProxy must have exactly one of Director or Rewrite set"))
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if p.Director != nil {
|
|
||||||
// p.Director(outreq)
|
|
||||||
// if outreq.Form != nil {
|
|
||||||
// outreq.URL.RawQuery = cleanQueryParams(outreq.URL.RawQuery)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
outreq.Close = false
|
outreq.Close = false
|
||||||
|
|
||||||
reqUpType := upgradeType(outreq.Header)
|
reqUpType := upgradeType(outreq.Header)
|
||||||
|
@ -379,8 +330,6 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
p.errorHandler(rw, req, fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType))
|
p.errorHandler(rw, req, fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// NOTE: removed
|
|
||||||
// removeHopByHopHeaders(outreq.Header)
|
|
||||||
|
|
||||||
// Issue 21096: tell backend applications that care about trailer support
|
// Issue 21096: tell backend applications that care about trailer support
|
||||||
// that we support trailers. (We do, but we don't go out of our way to
|
// that we support trailers. (We do, but we don't go out of our way to
|
||||||
|
@ -398,41 +347,17 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
outreq.Header.Set("Upgrade", reqUpType)
|
outreq.Header.Set("Upgrade", reqUpType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: removed
|
|
||||||
// if p.Rewrite != nil {
|
|
||||||
// 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-For")
|
||||||
// outreq.Header.Del("X-Forwarded-Host")
|
// outreq.Header.Del("X-Forwarded-Host")
|
||||||
// outreq.Header.Del("X-Forwarded-Proto")
|
// outreq.Header.Del("X-Forwarded-Proto")
|
||||||
|
|
||||||
// NOTE: removed
|
|
||||||
// Remove unparsable query parameters from the outbound request.
|
|
||||||
// outreq.URL.RawQuery = cleanQueryParams(outreq.URL.RawQuery)
|
|
||||||
pr := &ProxyRequest{
|
pr := &ProxyRequest{
|
||||||
In: req,
|
In: req,
|
||||||
Out: outreq,
|
Out: outreq,
|
||||||
}
|
}
|
||||||
p.Rewrite(pr)
|
p.Rewrite(pr)
|
||||||
outreq = pr.Out
|
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 _, ok := outreq.Header["User-Agent"]; !ok {
|
if _, ok := outreq.Header["User-Agent"]; !ok {
|
||||||
// If the outbound request doesn't have a User-Agent header set,
|
// If the outbound request doesn't have a User-Agent header set,
|
||||||
|
@ -473,9 +398,6 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: removed
|
|
||||||
// removeHopByHopHeaders(res.Header)
|
|
||||||
|
|
||||||
if !p.modifyResponse(rw, res, outreq) {
|
if !p.modifyResponse(rw, res, outreq) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -495,8 +417,6 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
rw.WriteHeader(res.StatusCode)
|
rw.WriteHeader(res.StatusCode)
|
||||||
|
|
||||||
// NOTE: changing this line extremely improve throughput
|
|
||||||
// err = p.copyResponse(rw, res.Body, p.flushInterval(res))
|
|
||||||
_, err = io.Copy(rw, res.Body)
|
_, err = io.Copy(rw, res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
@ -505,7 +425,7 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
// is abort the request. Issue 23643: ReverseProxy should use ErrAbortHandler
|
// is abort the request. Issue 23643: ReverseProxy should use ErrAbortHandler
|
||||||
// on read error while copying body.
|
// on read error while copying body.
|
||||||
// if !shouldPanicOnCopyError(req) {
|
// if !shouldPanicOnCopyError(req) {
|
||||||
// p.logf("suppressing panic for copyResponse error in test; copy error: %v", err)
|
// p.logf("suppressing panic for copyResponse error in test; copy error: %s", err)
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
panic(http.ErrAbortHandler)
|
panic(http.ErrAbortHandler)
|
||||||
|
@ -532,190 +452,6 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// var inOurTests bool // whether we're in our own tests
|
|
||||||
|
|
||||||
// NOTE: removed
|
|
||||||
// shouldPanicOnCopyError reports whether the reverse proxy should
|
|
||||||
// panic with http.ErrAbortHandler. This is the right thing to do by
|
|
||||||
// default, but Go 1.10 and earlier did not, so existing unit tests
|
|
||||||
// weren't expecting panics. Only panic in our own tests, or when
|
|
||||||
// running under the HTTP server.
|
|
||||||
// func shouldPanicOnCopyError(req *http.Request) bool {
|
|
||||||
// if inOurTests {
|
|
||||||
// // Our tests know to handle this panic.
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
// if req.Context().Value(http.ServerContextKey) != nil {
|
|
||||||
// // We seem to be running under an HTTP server, so
|
|
||||||
// // it'll recover the panic.
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
// // Otherwise act like Go 1.10 and earlier to not break
|
|
||||||
// // existing tests.
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
|
|
||||||
// removeHopByHopHeaders removes hop-by-hop headers.
|
|
||||||
//
|
|
||||||
// func removeHopByHopHeaders(h http.Header) {
|
|
||||||
// // RFC 7230, section 6.1: Remove headers listed in the "Connection" header.
|
|
||||||
// for _, f := range h["Connection"] {
|
|
||||||
// for _, sf := range strings.Split(f, ",") {
|
|
||||||
// if sf = textproto.TrimString(sf); sf != "" {
|
|
||||||
// h.Del(sf)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // RFC 2616, section 13.5.1: Remove a set of known hop-by-hop headers.
|
|
||||||
// // This behavior is superseded by the RFC 7230 Connection header, but
|
|
||||||
// // preserve it for backwards compatibility.
|
|
||||||
// for _, f := range hopHeaders {
|
|
||||||
// h.Del(f)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// NOTE: removed
|
|
||||||
// flushInterval returns the p.FlushInterval value, conditionally
|
|
||||||
// overriding its value for a specific request/response.
|
|
||||||
// func (p *ReverseProxy) flushInterval(res *http.Response) time.Duration {
|
|
||||||
// resCT := res.Header.Get("Content-Type")
|
|
||||||
|
|
||||||
// // For Server-Sent Events responses, flush immediately.
|
|
||||||
// // The MIME type is defined in https://www.w3.org/TR/eventsource/#text-event-stream
|
|
||||||
// if baseCT, _, _ := mime.ParseMediaType(resCT); baseCT == "text/event-stream" {
|
|
||||||
// return -1 // negative means immediately
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // We might have the case of streaming for which Content-Length might be unset.
|
|
||||||
// if res.ContentLength == -1 {
|
|
||||||
// return -1
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return p.FlushInterval
|
|
||||||
// }
|
|
||||||
|
|
||||||
// NOTE: removed
|
|
||||||
// func (p *ReverseProxy) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration) error {
|
|
||||||
// var w io.Writer = dst
|
|
||||||
|
|
||||||
// if flushInterval != 0 {
|
|
||||||
// mlw := &maxLatencyWriter{
|
|
||||||
// dst: dst,
|
|
||||||
// flush: http.NewResponseController(dst).Flush,
|
|
||||||
// latency: flushInterval,
|
|
||||||
// }
|
|
||||||
// defer mlw.stop()
|
|
||||||
|
|
||||||
// // set up initial timer so headers get flushed even if body writes are delayed
|
|
||||||
// mlw.flushPending = true
|
|
||||||
// mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)
|
|
||||||
|
|
||||||
// w = mlw
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var buf []byte
|
|
||||||
// if p.BufferPool != nil {
|
|
||||||
// buf = p.BufferPool.Get()
|
|
||||||
// defer p.BufferPool.Put(buf)
|
|
||||||
// }
|
|
||||||
// _, err := p.copyBuffer(w, src, buf)
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// copyBuffer returns any write errors or non-EOF read errors, and the amount
|
|
||||||
// of bytes written.
|
|
||||||
|
|
||||||
// NOTE: removed
|
|
||||||
// func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) {
|
|
||||||
// if len(buf) == 0 {
|
|
||||||
// buf = make([]byte, 32*1024)
|
|
||||||
// }
|
|
||||||
// var written int64
|
|
||||||
// for {
|
|
||||||
// nr, rerr := src.Read(buf)
|
|
||||||
// if rerr != nil && rerr != io.EOF && rerr != context.Canceled {
|
|
||||||
// p.logf("httputil: ReverseProxy read error during body copy: %v", rerr)
|
|
||||||
// }
|
|
||||||
// if nr > 0 {
|
|
||||||
// nw, werr := dst.Write(buf[:nr])
|
|
||||||
// if nw > 0 {
|
|
||||||
// written += int64(nw)
|
|
||||||
// }
|
|
||||||
// if werr != nil {
|
|
||||||
// return written, werr
|
|
||||||
// }
|
|
||||||
// if nr != nw {
|
|
||||||
// return written, io.ErrShortWrite
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if rerr != nil {
|
|
||||||
// if rerr == io.EOF {
|
|
||||||
// rerr = nil
|
|
||||||
// }
|
|
||||||
// return written, rerr
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (p *ReverseProxy) logf(format string, args ...any) {
|
|
||||||
// if p.ErrorLog != nil {
|
|
||||||
// p.ErrorLog.Printf(format, args...)
|
|
||||||
// } else {
|
|
||||||
hrlog.Errorf(format, args...)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: removed
|
|
||||||
// type maxLatencyWriter struct {
|
|
||||||
// dst io.Writer
|
|
||||||
// flush func() error
|
|
||||||
// latency time.Duration // non-zero; negative means to flush immediately
|
|
||||||
|
|
||||||
// mu sync.Mutex // protects t, flushPending, and dst.Flush
|
|
||||||
// t *time.Timer
|
|
||||||
// flushPending bool
|
|
||||||
// }
|
|
||||||
|
|
||||||
// NOTE: removed
|
|
||||||
// func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
|
|
||||||
// m.mu.Lock()
|
|
||||||
// defer m.mu.Unlock()
|
|
||||||
// n, err = m.dst.Write(p)
|
|
||||||
// if m.latency < 0 {
|
|
||||||
// m.flush()
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// if m.flushPending {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// if m.t == nil {
|
|
||||||
// m.t = time.AfterFunc(m.latency, m.delayedFlush)
|
|
||||||
// } else {
|
|
||||||
// m.t.Reset(m.latency)
|
|
||||||
// }
|
|
||||||
// m.flushPending = true
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (m *maxLatencyWriter) delayedFlush() {
|
|
||||||
// m.mu.Lock()
|
|
||||||
// defer m.mu.Unlock()
|
|
||||||
// if !m.flushPending { // if stop was called but AfterFunc already started this goroutine
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// m.flush()
|
|
||||||
// m.flushPending = false
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (m *maxLatencyWriter) stop() {
|
|
||||||
// m.mu.Lock()
|
|
||||||
// defer m.mu.Unlock()
|
|
||||||
// m.flushPending = false
|
|
||||||
// if m.t != nil {
|
|
||||||
// m.t.Stop()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
func upgradeType(h http.Header) string {
|
func upgradeType(h http.Header) string {
|
||||||
if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") {
|
if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") {
|
||||||
return ""
|
return ""
|
||||||
|
@ -760,7 +496,7 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R
|
||||||
defer close(backConnCloseCh)
|
defer close(backConnCloseCh)
|
||||||
|
|
||||||
if hijackErr != nil {
|
if hijackErr != nil {
|
||||||
p.errorHandler(rw, req, fmt.Errorf("hijack failed on protocol switch: %v", hijackErr))
|
p.errorHandler(rw, req, fmt.Errorf("hijack failed on protocol switch: %w", hijackErr))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
@ -770,18 +506,15 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R
|
||||||
res.Header = rw.Header()
|
res.Header = rw.Header()
|
||||||
res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above
|
res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above
|
||||||
if err := res.Write(brw); err != nil {
|
if err := res.Write(brw); err != nil {
|
||||||
p.errorHandler(rw, req, fmt.Errorf("response write: %v", err))
|
p.errorHandler(rw, req, fmt.Errorf("response write: %s", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := brw.Flush(); err != nil {
|
if err := brw.Flush(); err != nil {
|
||||||
p.errorHandler(rw, req, fmt.Errorf("response flush: %v", err))
|
p.errorHandler(rw, req, fmt.Errorf("response flush: %s", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
errc := make(chan error, 1)
|
errc := make(chan error, 1)
|
||||||
// NOTE: removed
|
|
||||||
// spc := switchProtocolCopier{user: conn, backend: backConn}
|
|
||||||
// go spc.copyToBackend(errc)
|
|
||||||
// go spc.copyFromBackend(errc)
|
|
||||||
go func() {
|
go func() {
|
||||||
_, err := io.Copy(conn, backConn)
|
_, err := io.Copy(conn, backConn)
|
||||||
errc <- err
|
errc <- err
|
||||||
|
@ -793,57 +526,6 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R
|
||||||
<-errc
|
<-errc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: removed
|
|
||||||
// switchProtocolCopier exists so goroutines proxying data back and
|
|
||||||
// forth have nice names in stacks.
|
|
||||||
// type switchProtocolCopier struct {
|
|
||||||
// user, backend io.ReadWriter
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
|
|
||||||
// _, err := io.Copy(c.user, c.backend)
|
|
||||||
// errc <- err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
|
|
||||||
// _, err := io.Copy(c.backend, c.user)
|
|
||||||
// errc <- err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// NOTE: removed
|
|
||||||
// func cleanQueryParams(s string) string {
|
|
||||||
// reencode := func(s string) string {
|
|
||||||
// v, _ := url.ParseQuery(s)
|
|
||||||
// return v.Encode()
|
|
||||||
// }
|
|
||||||
// for i := 0; i < len(s); {
|
|
||||||
// switch s[i] {
|
|
||||||
// case ';':
|
|
||||||
// return reencode(s)
|
|
||||||
// case '%':
|
|
||||||
// if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
|
|
||||||
// return reencode(s)
|
|
||||||
// }
|
|
||||||
// i += 3
|
|
||||||
// default:
|
|
||||||
// i++
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return s
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func ishex(c byte) bool {
|
|
||||||
// switch {
|
|
||||||
// case '0' <= c && c <= '9':
|
|
||||||
// return true
|
|
||||||
// case 'a' <= c && c <= 'f':
|
|
||||||
// return true
|
|
||||||
// case 'A' <= c && c <= 'F':
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
|
|
||||||
func IsPrint(s string) bool {
|
func IsPrint(s string) bool {
|
||||||
for i := 0; i < len(s); i++ {
|
for i := 0; i < len(s); i++ {
|
||||||
if s[i] < ' ' || s[i] > '~' {
|
if s[i] < ' ' || s[i] > '~' {
|
||||||
|
@ -852,3 +534,5 @@ func IsPrint(s string) bool {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var logger = logrus.WithField("?", "http")
|
102
src/proxy/reverse_proxy_mod_test.go
Normal file
102
src/proxy/reverse_proxy_mod_test.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// "net/http"
|
||||||
|
// "net/url"
|
||||||
|
// "os"
|
||||||
|
// "reflect"
|
||||||
|
// "testing"
|
||||||
|
// "time"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// var proxy Entry
|
||||||
|
// 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{}, &proxy).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"}}
|
||||||
|
// proxy = Entry{
|
||||||
|
// 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"}
|
||||||
|
// proxy = Entry{
|
||||||
|
// 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])
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
8
src/route/constants.go
Normal file
8
src/route/constants.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const udpBufferSize = 1500
|
||||||
|
const streamStopListenTimeout = 1 * time.Second
|
166
src/route/http_route.go
Executable file
166
src/route/http_route.go
Executable file
|
@ -0,0 +1,166 @@
|
||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
P "github.com/yusing/go-proxy/proxy"
|
||||||
|
PT "github.com/yusing/go-proxy/proxy/fields"
|
||||||
|
F "github.com/yusing/go-proxy/utils/functional"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
HTTPRoute struct {
|
||||||
|
Alias PT.Alias `json:"alias"`
|
||||||
|
Subroutes HTTPSubroutes `json:"subroutes"`
|
||||||
|
|
||||||
|
mux *http.ServeMux
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTPSubroute struct {
|
||||||
|
TargetURL URL `json:"targetURL"`
|
||||||
|
Path PathKey `json:"path"`
|
||||||
|
|
||||||
|
proxy *P.ReverseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
URL struct {
|
||||||
|
*url.URL
|
||||||
|
}
|
||||||
|
PathKey = string
|
||||||
|
SubdomainKey = string
|
||||||
|
HTTPSubroutes = map[PathKey]HTTPSubroute
|
||||||
|
)
|
||||||
|
|
||||||
|
var httpRoutes = F.NewMap[SubdomainKey, *HTTPRoute]()
|
||||||
|
|
||||||
|
func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) {
|
||||||
|
var tr *http.Transport
|
||||||
|
if entry.NoTLSVerify {
|
||||||
|
tr = transportNoTLS
|
||||||
|
} else {
|
||||||
|
tr = transport
|
||||||
|
}
|
||||||
|
|
||||||
|
rp := P.NewReverseProxy(entry.URL, tr, entry)
|
||||||
|
|
||||||
|
httpRoutes.Lock()
|
||||||
|
var r *HTTPRoute
|
||||||
|
r, ok := httpRoutes.UnsafeGet(entry.Alias.String())
|
||||||
|
if !ok {
|
||||||
|
r = &HTTPRoute{
|
||||||
|
Alias: entry.Alias,
|
||||||
|
Subroutes: make(HTTPSubroutes),
|
||||||
|
mux: http.NewServeMux(),
|
||||||
|
}
|
||||||
|
httpRoutes.UnsafeSet(entry.Alias.String(), r)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := entry.Path.String()
|
||||||
|
if _, exists := r.Subroutes[path]; exists {
|
||||||
|
httpRoutes.Unlock()
|
||||||
|
return nil, E.Duplicated("path", path).Subject(entry.Alias)
|
||||||
|
}
|
||||||
|
r.mux.HandleFunc(path, rp.ServeHTTP)
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
httpRoutes.Unlock()
|
||||||
|
switch t := err.(type) {
|
||||||
|
case error:
|
||||||
|
// NOTE: likely path pattern error
|
||||||
|
return nil, E.From(t).Subject(entry.Alias)
|
||||||
|
default:
|
||||||
|
return nil, E.From(fmt.Errorf("%v", t)).Subject(entry.Alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sr := HTTPSubroute{
|
||||||
|
TargetURL: URL{entry.URL},
|
||||||
|
proxy: rp,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
rewrite := rp.Rewrite
|
||||||
|
|
||||||
|
if logrus.GetLevel() == logrus.DebugLevel {
|
||||||
|
l := logrus.WithField("alias", entry.Alias)
|
||||||
|
|
||||||
|
sr.proxy.Rewrite = func(pr *P.ProxyRequest) {
|
||||||
|
l.Debug("request URL: ", pr.In.Host, pr.In.URL.Path)
|
||||||
|
l.Debug("request headers: ", pr.In.Header)
|
||||||
|
rewrite(pr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sr.proxy.Rewrite = rewrite
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Subroutes[path] = sr
|
||||||
|
httpRoutes.Unlock()
|
||||||
|
return r, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HTTPRoute) Start() E.NestedError {
|
||||||
|
httpRoutes.Set(r.Alias.String(), r)
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HTTPRoute) Stop() E.NestedError {
|
||||||
|
httpRoutes.Delete(r.Alias.String())
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *HTTPRoute) GetSubroute(path PathKey) (HTTPSubroute, bool) {
|
||||||
|
sr, ok := r.Subroutes[path]
|
||||||
|
return sr, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u URL) MarshalText() (text []byte, err error) {
|
||||||
|
return []byte(u.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mux, err := findMux(r.Host, PathKey(r.URL.Path))
|
||||||
|
if err != nil {
|
||||||
|
err = E.Failure("request").
|
||||||
|
Subjectf("%s %s%s", r.Method, r.Host, r.URL.Path).
|
||||||
|
With(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
logrus.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mux.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findMux(host string, path PathKey) (*http.ServeMux, error) {
|
||||||
|
sd := strings.Split(host, ".")[0]
|
||||||
|
if r, ok := httpRoutes.UnsafeGet(sd); ok {
|
||||||
|
return r.mux, nil
|
||||||
|
}
|
||||||
|
return nil, E.NotExists("route", fmt.Sprintf("subdomain: %s, path: %s", sd, path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: default + per proxy
|
||||||
|
var (
|
||||||
|
transport = &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
KeepAlive: 60 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
MaxIdleConns: 1000,
|
||||||
|
MaxIdleConnsPerHost: 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
transportNoTLS = func() *http.Transport {
|
||||||
|
var clone = transport.Clone()
|
||||||
|
clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||||
|
return clone
|
||||||
|
}()
|
||||||
|
)
|
34
src/route/route.go
Executable file
34
src/route/route.go
Executable file
|
@ -0,0 +1,34 @@
|
||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
M "github.com/yusing/go-proxy/models"
|
||||||
|
P "github.com/yusing/go-proxy/proxy"
|
||||||
|
F "github.com/yusing/go-proxy/utils/functional"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Route interface {
|
||||||
|
Start() E.NestedError
|
||||||
|
Stop() E.NestedError
|
||||||
|
}
|
||||||
|
Routes = F.Map[string, Route]
|
||||||
|
)
|
||||||
|
|
||||||
|
// function alias
|
||||||
|
var NewRoutes = F.NewMap[string, Route]
|
||||||
|
|
||||||
|
func NewRoute(en *M.ProxyEntry) (Route, E.NestedError) {
|
||||||
|
entry, err := P.NewEntry(en)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch e := entry.(type) {
|
||||||
|
case *P.StreamEntry:
|
||||||
|
return NewStreamRoute(e)
|
||||||
|
case *P.Entry:
|
||||||
|
return NewHTTPRoute(e)
|
||||||
|
default:
|
||||||
|
panic("bug: should not reach here")
|
||||||
|
}
|
||||||
|
}
|
131
src/route/stream_route.go
Executable file
131
src/route/stream_route.go
Executable file
|
@ -0,0 +1,131 @@
|
||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
P "github.com/yusing/go-proxy/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StreamRoute struct {
|
||||||
|
*P.StreamEntry
|
||||||
|
StreamImpl `json:"-"`
|
||||||
|
|
||||||
|
wg sync.WaitGroup
|
||||||
|
stopCh chan struct{}
|
||||||
|
connCh chan any
|
||||||
|
started atomic.Bool
|
||||||
|
l logrus.FieldLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreamImpl interface {
|
||||||
|
Setup() error
|
||||||
|
Accept() (any, error)
|
||||||
|
Handle(any) error
|
||||||
|
CloseListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStreamRoute(entry *P.StreamEntry) (*StreamRoute, E.NestedError) {
|
||||||
|
// TODO: support non-coherent scheme
|
||||||
|
if !entry.Scheme.IsCoherent() {
|
||||||
|
return nil, E.Unsupported("scheme", fmt.Sprintf("%v -> %v", entry.Scheme.ListeningScheme, entry.Scheme.ProxyScheme))
|
||||||
|
}
|
||||||
|
base := &StreamRoute{
|
||||||
|
StreamEntry: entry,
|
||||||
|
wg: sync.WaitGroup{},
|
||||||
|
stopCh: make(chan struct{}, 1),
|
||||||
|
connCh: make(chan any),
|
||||||
|
l: logger.WithField("alias", entry.Alias),
|
||||||
|
}
|
||||||
|
if entry.Scheme.ListeningScheme.IsTCP() {
|
||||||
|
base.StreamImpl = NewTCPRoute(base)
|
||||||
|
} else {
|
||||||
|
base.StreamImpl = NewUDPRoute(base)
|
||||||
|
}
|
||||||
|
return base, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StreamRoute) Start() E.NestedError {
|
||||||
|
if r.started.Load() {
|
||||||
|
return E.ErrAlreadyStarted
|
||||||
|
}
|
||||||
|
r.wg.Wait()
|
||||||
|
if err := r.Setup(); err != nil {
|
||||||
|
return E.Failure("setup").With(err)
|
||||||
|
}
|
||||||
|
r.started.Store(true)
|
||||||
|
r.wg.Add(2)
|
||||||
|
go r.grAcceptConnections()
|
||||||
|
go r.grHandleConnections()
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StreamRoute) Stop() E.NestedError {
|
||||||
|
if !r.started.Load() {
|
||||||
|
return E.ErrNotStarted
|
||||||
|
}
|
||||||
|
l := r.l
|
||||||
|
close(r.stopCh)
|
||||||
|
r.CloseListeners()
|
||||||
|
|
||||||
|
done := make(chan struct{}, 1)
|
||||||
|
go func() {
|
||||||
|
r.wg.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
l.Info("stopped listening")
|
||||||
|
case <-time.After(streamStopListenTimeout):
|
||||||
|
l.Error("timed out waiting for connections")
|
||||||
|
}
|
||||||
|
return E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StreamRoute) grAcceptConnections() {
|
||||||
|
defer r.wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.stopCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
conn, err := r.Accept()
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case <-r.stopCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
r.l.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.connCh <- conn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StreamRoute) grHandleConnections() {
|
||||||
|
defer r.wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.stopCh:
|
||||||
|
return
|
||||||
|
case conn := <-r.connCh:
|
||||||
|
go func() {
|
||||||
|
err := r.Handle(conn)
|
||||||
|
if err != nil {
|
||||||
|
r.l.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = logrus.WithField("?", "stream")
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package route
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -6,29 +6,31 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
U "github.com/yusing/go-proxy/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tcpDialTimeout = 5 * time.Second
|
const tcpDialTimeout = 5 * time.Second
|
||||||
|
|
||||||
type Pipes []*BidirectionalPipe
|
type Pipes []*U.BidirectionalPipe
|
||||||
|
|
||||||
type TCPRoute struct {
|
type TCPRoute struct {
|
||||||
*StreamRouteBase
|
*StreamRoute
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
pipe Pipes
|
pipe Pipes
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTCPRoute(base *StreamRouteBase) StreamImpl {
|
func NewTCPRoute(base *StreamRoute) StreamImpl {
|
||||||
return &TCPRoute{
|
return &TCPRoute{
|
||||||
StreamRouteBase: base,
|
StreamRoute: base,
|
||||||
listener: nil,
|
listener: nil,
|
||||||
pipe: make(Pipes, 0),
|
pipe: make(Pipes, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (route *TCPRoute) Setup() error {
|
func (route *TCPRoute) Setup() error {
|
||||||
in, err := net.Listen("tcp", fmt.Sprintf(":%v", route.ListeningPort))
|
in, err := net.Listen("tcp", fmt.Sprintf(":%v", route.Port.ListeningPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -48,10 +50,10 @@ func (route *TCPRoute) Handle(c interface{}) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tcpDialTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), tcpDialTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
serverAddr := fmt.Sprintf("%s:%v", route.TargetHost, route.TargetPort)
|
serverAddr := fmt.Sprintf("%s:%v", route.Host, route.Port.ProxyPort)
|
||||||
dialer := &net.Dialer{}
|
dialer := &net.Dialer{}
|
||||||
|
|
||||||
serverConn, err := dialer.DialContext(ctx, route.TargetScheme, serverAddr)
|
serverConn, err := dialer.DialContext(ctx, route.Scheme.ProxyScheme.String(), serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -63,7 +65,7 @@ func (route *TCPRoute) Handle(c interface{}) error {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
route.mu.Lock()
|
route.mu.Lock()
|
||||||
pipe := NewBidirectionalPipe(pipeCtx, clientConn, serverConn)
|
pipe := U.NewBidirectionalPipe(pipeCtx, clientConn, serverConn)
|
||||||
route.pipe = append(route.pipe, pipe)
|
route.pipe = append(route.pipe, pipe)
|
||||||
route.mu.Unlock()
|
route.mu.Unlock()
|
||||||
return pipe.Start()
|
return pipe.Start()
|
||||||
|
@ -76,7 +78,7 @@ func (route *TCPRoute) CloseListeners() {
|
||||||
route.listener.Close()
|
route.listener.Close()
|
||||||
route.listener = nil
|
route.listener = nil
|
||||||
for _, pipe := range route.pipe {
|
for _, pipe := range route.pipe {
|
||||||
if err := pipe.Stop(); err != nil {
|
if err := pipe.Stop(); err.IsNotNil() {
|
||||||
route.l.Error(err)
|
route.l.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package route
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -6,10 +6,12 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UDPRoute struct {
|
type UDPRoute struct {
|
||||||
*StreamRouteBase
|
*StreamRoute
|
||||||
|
|
||||||
connMap UDPConnMap
|
connMap UDPConnMap
|
||||||
connMapMutex sync.Mutex
|
connMapMutex sync.Mutex
|
||||||
|
@ -21,28 +23,28 @@ type UDPRoute struct {
|
||||||
type UDPConn struct {
|
type UDPConn struct {
|
||||||
src *net.UDPConn
|
src *net.UDPConn
|
||||||
dst *net.UDPConn
|
dst *net.UDPConn
|
||||||
*BidirectionalPipe
|
*utils.BidirectionalPipe
|
||||||
}
|
}
|
||||||
|
|
||||||
type UDPConnMap map[string]*UDPConn
|
type UDPConnMap map[string]*UDPConn
|
||||||
|
|
||||||
func NewUDPRoute(base *StreamRouteBase) StreamImpl {
|
func NewUDPRoute(base *StreamRoute) StreamImpl {
|
||||||
return &UDPRoute{
|
return &UDPRoute{
|
||||||
StreamRouteBase: base,
|
StreamRoute: base,
|
||||||
connMap: make(UDPConnMap),
|
connMap: make(UDPConnMap),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (route *UDPRoute) Setup() error {
|
func (route *UDPRoute) Setup() error {
|
||||||
laddr, err := net.ResolveUDPAddr(route.ListeningScheme, fmt.Sprintf(":%v", route.ListeningPort))
|
laddr, err := net.ResolveUDPAddr(route.Scheme.ListeningScheme.String(), fmt.Sprintf(":%v", route.Port.ProxyPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
source, err := net.ListenUDP(route.ListeningScheme, laddr)
|
source, err := net.ListenUDP(route.Scheme.ListeningScheme.String(), laddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
raddr, err := net.ResolveUDPAddr(route.TargetScheme, fmt.Sprintf("%s:%v", route.TargetHost, route.TargetPort))
|
raddr, err := net.ResolveUDPAddr(route.Scheme.ProxyScheme.String(), fmt.Sprintf("%s:%v", route.Host, route.Port.ProxyPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
source.Close()
|
source.Close()
|
||||||
return err
|
return err
|
||||||
|
@ -90,7 +92,7 @@ func (route *UDPRoute) Accept() (interface{}, error) {
|
||||||
conn = &UDPConn{
|
conn = &UDPConn{
|
||||||
srcConn,
|
srcConn,
|
||||||
dstConn,
|
dstConn,
|
||||||
NewBidirectionalPipe(pipeCtx, sourceRWCloser{in, dstConn}, sourceRWCloser{in, srcConn}),
|
utils.NewBidirectionalPipe(pipeCtx, sourceRWCloser{in, dstConn}, sourceRWCloser{in, srcConn}),
|
||||||
}
|
}
|
||||||
route.connMap[key] = conn
|
route.connMap[key] = conn
|
||||||
}
|
}
|
||||||
|
@ -112,10 +114,10 @@ func (route *UDPRoute) CloseListeners() {
|
||||||
}
|
}
|
||||||
for _, conn := range route.connMap {
|
for _, conn := range route.connMap {
|
||||||
if err := conn.src.Close(); err != nil {
|
if err := conn.src.Close(); err != nil {
|
||||||
route.l.Errorf("error closing src conn: %w", err)
|
route.l.Errorf("error closing src conn: %s", err)
|
||||||
}
|
}
|
||||||
if err := conn.dst.Close(); err != nil {
|
if err := conn.dst.Close(); err != nil {
|
||||||
route.l.Error("error closing dst conn: %w", err)
|
route.l.Error("error closing dst conn: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
route.connMap = make(UDPConnMap)
|
route.connMap = make(UDPConnMap)
|
25
src/server/instance.go
Normal file
25
src/server/instance.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
var proxyServer, apiServer *server
|
||||||
|
|
||||||
|
func InitProxyServer(opt Options) *server {
|
||||||
|
if proxyServer == nil {
|
||||||
|
proxyServer = NewServer(opt)
|
||||||
|
}
|
||||||
|
return proxyServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitAPIServer(opt Options) *server {
|
||||||
|
if apiServer == nil {
|
||||||
|
apiServer = NewServer(opt)
|
||||||
|
}
|
||||||
|
return apiServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetProxyServer() *server {
|
||||||
|
return proxyServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAPIServer() *server {
|
||||||
|
return apiServer
|
||||||
|
}
|
157
src/server/server.go
Normal file
157
src/server/server.go
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/yusing/go-proxy/autocert"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
Name string
|
||||||
|
CertProvider *autocert.Provider
|
||||||
|
http *http.Server
|
||||||
|
https *http.Server
|
||||||
|
httpStarted bool
|
||||||
|
httpsStarted bool
|
||||||
|
startTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Name string
|
||||||
|
// port (with leading colon)
|
||||||
|
HTTPPort string
|
||||||
|
// port (with leading colon)
|
||||||
|
HTTPSPort string
|
||||||
|
CertProvider *autocert.Provider
|
||||||
|
RedirectToHTTPS bool
|
||||||
|
Handler http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogrusWrapper struct {
|
||||||
|
*logrus.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LogrusWrapper) Write(b []byte) (int, error) {
|
||||||
|
return l.Logger.WriterLevel(logrus.ErrorLevel).Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(opt Options) (s *server) {
|
||||||
|
var httpSer, httpsSer *http.Server
|
||||||
|
var httpHandler http.Handler
|
||||||
|
|
||||||
|
logger := log.Default()
|
||||||
|
logger.SetOutput(LogrusWrapper{
|
||||||
|
logrus.WithFields(logrus.Fields{"?": "server", "name": opt.Name}),
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := opt.CertProvider.GetCert(nil)
|
||||||
|
certAvailable := err == nil
|
||||||
|
if certAvailable && opt.RedirectToHTTPS && opt.HTTPSPort != "" {
|
||||||
|
httpHandler = redirectToTLSHandler(opt.HTTPSPort)
|
||||||
|
} else {
|
||||||
|
httpHandler = opt.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt.HTTPPort != "" {
|
||||||
|
httpSer = &http.Server{
|
||||||
|
Addr: opt.HTTPPort,
|
||||||
|
Handler: httpHandler,
|
||||||
|
ErrorLog: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if certAvailable && opt.HTTPSPort != "" {
|
||||||
|
httpsSer = &http.Server{
|
||||||
|
Addr: opt.HTTPSPort,
|
||||||
|
Handler: opt.Handler,
|
||||||
|
ErrorLog: logger,
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
GetCertificate: opt.CertProvider.GetCert,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &server{
|
||||||
|
Name: opt.Name,
|
||||||
|
CertProvider: opt.CertProvider,
|
||||||
|
http: httpSer,
|
||||||
|
https: httpsSer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) Start() {
|
||||||
|
if s.http == nil && s.https == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.startTime = time.Now()
|
||||||
|
if s.http != nil {
|
||||||
|
s.httpStarted = true
|
||||||
|
logrus.Printf("starting http %s server on %s", s.Name, s.http.Addr)
|
||||||
|
go func() {
|
||||||
|
s.handleErr("http", s.http.ListenAndServe())
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.https != nil {
|
||||||
|
s.httpsStarted = true
|
||||||
|
logrus.Printf("starting https %s server on %s", s.Name, s.https.Addr)
|
||||||
|
go func() {
|
||||||
|
s.handleErr("https", s.https.ListenAndServeTLS(s.CertProvider.GetCertPath(), s.CertProvider.GetKeyPath()))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) Stop() {
|
||||||
|
if s.http == nil && s.https == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if s.http != nil && s.httpStarted {
|
||||||
|
s.handleErr("http", s.http.Shutdown(ctx))
|
||||||
|
s.httpStarted = false
|
||||||
|
logger.Debugf("HTTP server %q stopped", s.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.https != nil && s.httpsStarted {
|
||||||
|
s.handleErr("https", s.https.Shutdown(ctx))
|
||||||
|
s.httpsStarted = false
|
||||||
|
logger.Debugf("HTTPS server %q stopped", s.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) Uptime() time.Duration {
|
||||||
|
return time.Since(s.startTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleErr(scheme string, err error) {
|
||||||
|
switch err {
|
||||||
|
case nil, http.ErrServerClosed:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
logrus.Fatalf("failed to start %s %s server: %s", scheme, s.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func redirectToTLSHandler(port string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.URL.Scheme = "https"
|
||||||
|
r.URL.Host = r.URL.Hostname() + port
|
||||||
|
|
||||||
|
var redirectCode int
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
redirectCode = http.StatusMovedPermanently
|
||||||
|
} else {
|
||||||
|
redirectCode = http.StatusPermanentRedirect
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, r.URL.String(), redirectCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = logrus.WithField("?", "server")
|
50
src/utils/format.go
Normal file
50
src/utils/format.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FormatDuration(d time.Duration) string {
|
||||||
|
// Get total seconds from duration
|
||||||
|
totalSeconds := int64(d.Seconds())
|
||||||
|
|
||||||
|
// Calculate days, hours, minutes, and seconds
|
||||||
|
days := totalSeconds / (24 * 3600)
|
||||||
|
hours := (totalSeconds % (24 * 3600)) / 3600
|
||||||
|
minutes := (totalSeconds % 3600) / 60
|
||||||
|
seconds := totalSeconds % 60
|
||||||
|
|
||||||
|
// Create a slice to hold parts of the duration
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
if days > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d Day%s", days, pluralize(days)))
|
||||||
|
}
|
||||||
|
if hours > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d Hour%s", hours, pluralize(hours)))
|
||||||
|
}
|
||||||
|
if minutes > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d Minute%s", minutes, pluralize(minutes)))
|
||||||
|
}
|
||||||
|
if seconds > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d Second%s", seconds, pluralize(seconds)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join the parts with appropriate connectors
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "0 Seconds"
|
||||||
|
}
|
||||||
|
if len(parts) == 1 {
|
||||||
|
return parts[0]
|
||||||
|
}
|
||||||
|
return strings.Join(parts[:len(parts)-1], ", ") + " and " + parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluralize(n int64) string {
|
||||||
|
if n > 1 {
|
||||||
|
return "s"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue