mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 22:30:47 +01:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
996b418ea9 | ||
|
|
4cddd4ff71 | ||
|
|
7a0478164f | ||
|
|
2e7ba51521 | ||
|
|
5be8659a99 | ||
|
|
719693deb7 | ||
|
|
23e7d06081 | ||
|
|
85fb637551 | ||
|
|
2fc82c3790 | ||
|
|
a5a31a0d63 | ||
|
|
73e481bc96 | ||
|
|
93359110a2 | ||
|
|
24778d1093 | ||
|
|
830d0bdadd | ||
|
|
e12b356d0d | ||
|
|
52549b6446 | ||
|
|
8694987ef9 | ||
|
|
b125b14bf6 | ||
|
|
c782f365f9 | ||
|
|
72418a2056 | ||
|
|
03bf425a38 | ||
|
|
5fafa619ee | ||
|
|
bebf99ed6c | ||
|
|
8483263d01 | ||
|
|
351bf84559 | ||
|
|
cbe23d2ed1 | ||
|
|
6e45f3683c | ||
|
|
581894c05b | ||
|
|
2657b1f726 | ||
|
|
3505e8ff7e | ||
|
|
2314e39291 | ||
|
|
bd19f443d4 | ||
|
|
ce433f0c51 | ||
|
|
47877e5119 | ||
|
|
486122f3d8 | ||
|
|
a0be1f11d3 | ||
|
|
662190e09e | ||
|
|
ce1e5da72e | ||
|
|
eb7e744a75 | ||
|
|
ac26baf97f | ||
|
|
5a8c11de16 | ||
|
|
a8ecafcd09 | ||
|
|
af37d1f29e | ||
|
|
8cfd24e6bd | ||
|
|
7bf5784016 | ||
|
|
25930a1a73 | ||
|
|
f20a1ff523 | ||
|
|
ba51796a64 | ||
|
|
c445d50221 | ||
|
|
73dfc17a82 | ||
|
|
fdab026a3b | ||
|
|
c789c69c86 | ||
|
|
2b298aa7fa | ||
|
|
d20e4d435a | ||
|
|
15d9436d52 | ||
|
|
ca98b31458 | ||
|
|
77f957c7a8 | ||
|
|
51493c9fdd | ||
|
|
9b34dc994d | ||
|
|
6bc4c1c49a | ||
|
|
443dd99b5b | ||
|
|
db6f857aaf | ||
|
|
6a54fc85ac | ||
|
|
90f4aac946 |
21
.github/workflows/docker-image.yml
vendored
Normal file
21
.github/workflows/docker-image.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
jobs:
|
||||
build_and_push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Docker Build and Push
|
||||
id: docker_build_push
|
||||
uses: GlueOps/github-actions-build-push-containers@v0.3.7
|
||||
with:
|
||||
tags: ${{ github.ref_name }}
|
||||
|
||||
- name: Tag as latest
|
||||
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||
run: |
|
||||
docker tag ghcr.io/${{ github.repository }}:${{ github.ref_name }} ghcr.io/${{ github.repository }}:latest
|
||||
docker push ghcr.io/${{ github.repository }}:latest
|
||||
21
.gitignore
vendored
21
.gitignore
vendored
@@ -1,7 +1,18 @@
|
||||
compose.yml
|
||||
go-proxy.yml
|
||||
config.yml
|
||||
providers.yml
|
||||
bin/go-proxy.bak
|
||||
|
||||
config/
|
||||
certs/
|
||||
bin/
|
||||
|
||||
templates/codemirror/
|
||||
|
||||
logs/
|
||||
log/
|
||||
log/
|
||||
|
||||
.vscode/settings.json
|
||||
|
||||
go.work.sum
|
||||
|
||||
!src/config/
|
||||
|
||||
todo.md
|
||||
15
.gitlab-ci.yml
Normal file
15
.gitlab-ci.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
build-image:
|
||||
image: docker
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
|
||||
variables:
|
||||
CI_REGISTRY_IMAGE: $CI_REGISTRY_IMAGE:latest
|
||||
- if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH
|
||||
variables:
|
||||
CI_REGISTRY_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH
|
||||
before_script:
|
||||
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
|
||||
script:
|
||||
- echo building $CI_REGISTRY_IMAGE
|
||||
- docker build --pull -t $CI_REGISTRY_IMAGE .
|
||||
- docker push $CI_REGISTRY_IMAGE
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "frontend"]
|
||||
path = frontend
|
||||
url = https://github.com/yusing/go-proxy-frontend
|
||||
13
.vscode/settings.example.json
vendored
Normal file
13
.vscode/settings.example.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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",
|
||||
"providers.yml"
|
||||
]
|
||||
}
|
||||
}
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"go.inferGopath": false
|
||||
}
|
||||
31
Dockerfile
31
Dockerfile
@@ -1,22 +1,29 @@
|
||||
FROM golang:1.23.1-alpine AS builder
|
||||
COPY src /src
|
||||
ENV GOCACHE=/root/.cache/go-build
|
||||
WORKDIR /src
|
||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||
go mod download
|
||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||
--mount=type=cache,target="/root/.cache/go-build" \
|
||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o go-proxy github.com/yusing/go-proxy
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
LABEL maintainer="yusing@6uo.me"
|
||||
|
||||
RUN apk add --no-cache bash tzdata
|
||||
RUN mkdir /app
|
||||
COPY bin/go-proxy entrypoint.sh /app/
|
||||
COPY templates/ /app/templates
|
||||
COPY config.example.yml /app/config.yml
|
||||
RUN apk add --no-cache tzdata
|
||||
# copy binary
|
||||
COPY --from=builder /src/go-proxy /app/
|
||||
COPY schema/ /app/schema
|
||||
|
||||
RUN chmod +x /app/go-proxy /app/entrypoint.sh
|
||||
ENV DOCKER_HOST unix:///var/run/docker.sock
|
||||
ENV GOPROXY_DEBUG 0
|
||||
ENV GOPROXY_REDIRECT_HTTP 1
|
||||
RUN chmod +x /app/go-proxy
|
||||
ENV DOCKER_HOST=unix:///var/run/docker.sock
|
||||
ENV GOPROXY_DEBUG=0
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 8080
|
||||
EXPOSE 8888
|
||||
EXPOSE 443
|
||||
EXPOSE 8443
|
||||
|
||||
WORKDIR /app
|
||||
ENTRYPOINT /app/entrypoint.sh
|
||||
CMD ["/app/go-proxy"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 [fullname]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
44
Makefile
44
Makefile
@@ -2,34 +2,38 @@
|
||||
|
||||
all: build quick-restart logs
|
||||
|
||||
setup:
|
||||
mkdir -p config certs
|
||||
[ -f config/config.yml ] || cp config.example.yml config/config.yml
|
||||
[ -f config/providers.yml ] || touch config/providers.yml
|
||||
|
||||
build:
|
||||
mkdir -p bin
|
||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy src/go-proxy/*.go
|
||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy github.com/yusing/go-proxy
|
||||
|
||||
test:
|
||||
cd src && go test ./... && cd ..
|
||||
|
||||
up:
|
||||
docker compose up -d --build go-proxy
|
||||
|
||||
quick-restart: # quick restart without restarting the container
|
||||
docker cp bin/go-proxy go-proxy:/app/go-proxy
|
||||
docker cp templates/* go-proxy:/app/templates
|
||||
docker cp entrypoint.sh go-proxy:/app/entrypoint.sh
|
||||
docker exec -d go-proxy bash /app/entrypoint.sh restart
|
||||
docker compose up -d
|
||||
|
||||
restart:
|
||||
docker kill go-proxy
|
||||
docker compose up -d go-proxy
|
||||
docker compose restart -t 0
|
||||
|
||||
logs:
|
||||
tail -f log/go-proxy.log
|
||||
docker compose logs -f
|
||||
|
||||
get:
|
||||
go get -d -u ./src/go-proxy
|
||||
cd src && go get -u && go mod tidy && cd ..
|
||||
|
||||
udp-server:
|
||||
docker run -it --rm \
|
||||
-p 9999:9999/udp \
|
||||
--label proxy.test-udp.scheme=udp \
|
||||
--label proxy.test-udp.port=20003:9999 \
|
||||
--network data_default \
|
||||
--name test-udp \
|
||||
$$(docker build -q -f udp-test-server.Dockerfile .)
|
||||
debug:
|
||||
make build && GOPROXY_DEBUG=1 bin/go-proxy
|
||||
|
||||
archive:
|
||||
git archive HEAD -o ../go-proxy-$$(date +"%Y%m%d%H%M").zip
|
||||
|
||||
repush:
|
||||
git reset --soft HEAD^
|
||||
git add -A
|
||||
git commit -m "repush"
|
||||
git push gitlab dev --force
|
||||
431
README.md
431
README.md
@@ -1,386 +1,123 @@
|
||||
# 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 -->
|
||||
|
||||
- [Key Points](#key-points)
|
||||
- [How to use](#how-to-use)
|
||||
- [Binary](#binary)
|
||||
- [Docker](#docker)
|
||||
- [Configuration](#configuration)
|
||||
- [Labels](#labels)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Config File](#config-file)
|
||||
- [Provider File](#provider-file)
|
||||
- [Supported Cert Providers](#supported-cert-providers)
|
||||
- [Examples](#examples)
|
||||
- [Single Port Configuration](#single-port-configuration-example)
|
||||
- [Multiple Ports Configuration](#multiple-ports-configuration-example)
|
||||
- [TCP/UDP Configuration](#tcpudp-configuration-example)
|
||||
- [Load balancing Configuration](#load-balancing-configuration-example)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Benchmarks](#benchmarks)
|
||||
- [Memory usage](#memory-usage)
|
||||
- [Build it yourself](#build-it-yourself)
|
||||
- [go-proxy](#go-proxy)
|
||||
- [Key Points](#key-points)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Commands line arguments](#commands-line-arguments)
|
||||
- [Environment variables](#environment-variables)
|
||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
||||
- [Config File](#config-file)
|
||||
- [Provider File](#provider-file)
|
||||
- [Known issues](#known-issues)
|
||||
- [Build it yourself](#build-it-yourself)
|
||||
|
||||
## Key Points
|
||||
|
||||
- fast, nearly no performance penalty for end users when comparing to direct IP connections (See [benchmarks](#benchmarks))
|
||||
- auto detect reverse proxies from docker
|
||||
- additional reverse proxies from provider yaml file
|
||||
- allow multiple docker / file providers by custom `config.yml` file
|
||||
- auto certificate obtaining and renewal (See [Config File](#config-file) and [Supported Cert Providers](#supported-cert-providers))
|
||||
- subdomain matching **(domain name doesn't matter)**
|
||||
- path matching
|
||||
- HTTP proxy
|
||||
- TCP/UDP Proxy
|
||||
- HTTP round robin load balance support (same subdomain and path across different hosts)
|
||||
- Auto hot-reload on container start / die / stop or config changes.
|
||||
- Simple panel to see all reverse proxies and health (visit port [panel port] of go-proxy `https://*.y.z:[panel port]`)
|
||||
- Easy to use
|
||||
- Auto certificate obtaining and renewal (See [Supported DNS Challenge Providers](docs/dns_providers.md))
|
||||
- Auto configuration for docker contaienrs
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- Support HTTP(s), TCP and UDP
|
||||
- Support HTTP(s) round robin load balancing
|
||||
- Web UI for configuration and monitoring (See [screenshots](screeenshots))
|
||||
- Written in **[Go](https://go.dev)**
|
||||
|
||||
- you can customize it by modifying [templates/panel.html](templates/panel.html)
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||

|
||||
## Getting Started
|
||||
|
||||
## How to use
|
||||
1. Setup DNS Records
|
||||
|
||||
1. Download and extract the latest release (or clone the repository if you want to try out experimental features)
|
||||
- A Record: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||
|
||||
2. Copy `config.example.yml` to `config.yml` and modify the content to fit your needs
|
||||
2. Setup `go-proxy` [See here](docs/docker.md)
|
||||
|
||||
3. Do the same for `providers.example.yml`
|
||||
3. Configure `go-proxy`
|
||||
- with text editor (i.e. Visual Studio Code)
|
||||
- or with web config editor via `http://gp.y.z`
|
||||
|
||||
4. See [Binary](#binary) or [docker](#docker)
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Binary
|
||||
### Commands line arguments
|
||||
|
||||
1. (Optional) enabled HTTPS
|
||||
| Argument | Description |
|
||||
| ---------- | -------------------------------- |
|
||||
| empty | start proxy server |
|
||||
| `validate` | validate config and exit |
|
||||
| `reload` | trigger a force reload of config |
|
||||
|
||||
- Use autocert feature by completing `autocert` in `config.yml`
|
||||
|
||||
- Use existing certificate
|
||||
|
||||
Prepare your wildcard (`*.y.z`) SSL cert in `certs/`
|
||||
|
||||
- cert / chain / fullchain: `./certs/cert.crt`
|
||||
- private key: `./certs/priv.key`
|
||||
|
||||
2. run the binary `bin/go-proxy`
|
||||
|
||||
3. enjoy
|
||||
|
||||
### Docker
|
||||
|
||||
1. Copy content from [compose.example.yml](compose.example.yml) and create your own `compose.yml`
|
||||
|
||||
2. Add networks to make sure it is in the same network with other containers, or make sure `proxy.<alias>.host` is reachable
|
||||
|
||||
3. (Optional) enable HTTPS
|
||||
|
||||
- Use autocert feature
|
||||
|
||||
1. mount `./certs` to `/app/certs`
|
||||
```yaml
|
||||
go-proxy:
|
||||
...
|
||||
volumes:
|
||||
- ./certs:/app/certs
|
||||
```
|
||||
2. complete `autocert` in `config.yml`
|
||||
|
||||
- Use existing certificate
|
||||
|
||||
Mount your wildcard (`*.y.z`) SSL cert to enable https. See [Getting SSL Certs](#getting-ssl-certs)
|
||||
|
||||
- cert / chain / fullchain -> `/app/certs/cert.crt`
|
||||
- private key -> `/app/certs/priv.key`
|
||||
|
||||
4. Start `go-proxy` with `docker compose up -d` or `make up`.
|
||||
|
||||
5. (Optional) If you are using ufw with vpn that drop all inbound traffic except vpn, run below to allow docker containers to connect to `go-proxy`
|
||||
|
||||
In case the network of your container is in subnet `172.16.0.0/16` (bridge),
|
||||
and vpn network is under `100.64.0.0/10` (i.e. tailscale)
|
||||
|
||||
`sudo ufw allow from 172.16.0.0/16 to 100.64.0.0/10`
|
||||
|
||||
You can also list CIDRs of all docker bridge networks by:
|
||||
|
||||
`docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -`
|
||||
|
||||
6. start your docker app, and visit <container_name>.y.z
|
||||
|
||||
7. check the logs with `docker compose logs` or `make logs` to see if there is any error, check panel at [panel port] for active proxies
|
||||
|
||||
## Known issues
|
||||
|
||||
None
|
||||
|
||||
## Configuration
|
||||
|
||||
With container name, most of the time no label needs to be added.
|
||||
|
||||
### Labels
|
||||
|
||||
- `proxy.aliases`: comma separated aliases for subdomain matching
|
||||
- defaults to `container_name`
|
||||
- `proxy.*.<field>`: wildcard config for all aliases
|
||||
- `proxy.<alias>.scheme`: container port protocol (`http` or `https`)
|
||||
- defaults to `http`
|
||||
- `proxy.<alias>.host`: proxy host
|
||||
- defaults to `container_name`
|
||||
- `proxy.<alias>.port`: proxy port
|
||||
- http/https: defaults to first expose port (declared in `Dockerfile` or `docker-compose.yml`)
|
||||
- tcp/udp: is in format of `[<listeningPort>:]<targetPort>`
|
||||
- when `listeningPort` is omitted (not suggested), a free port will be used automatically.
|
||||
- `targetPort` must be a number, or the predefined names (see [constants.go:14](src/go-proxy/constants.go#L14))
|
||||
- `proxy.<alias>.no_tls_verify`: whether skip tls verify when scheme is https
|
||||
- defaults to false
|
||||
- `proxy.<alias>.path`: path matching (for http proxy only)
|
||||
- defaults to empty
|
||||
- `proxy.<alias>.path_mode`: mode for path handling
|
||||
|
||||
- defaults to empty
|
||||
- allowed: \<empty>, forward, sub
|
||||
- empty: remove path prefix from URL when proxying
|
||||
1. apps.y.z/webdav -> webdav:80
|
||||
2. apps.y.z./webdav/path/to/file -> webdav:80/path/to/file
|
||||
- forward: path remain unchanged
|
||||
1. apps.y.z/webdav -> webdav:80/webdav
|
||||
2. apps.y.z./webdav/path/to/file -> webdav:80/webdav/path/to/file
|
||||
- sub: (experimental) remove path prefix from URL and also append path to HTML link attributes (`src`, `href` and `action`) and Javascript `fetch(url)` by response body substitution
|
||||
e.g. apps.y.z/app1 -> webdav:80, `href="/path/to/file"` -> `href="/app1/path/to/file"`
|
||||
|
||||
- `proxy.<alias>.load_balance`: enable load balance
|
||||
- allowed: `1`, `true`
|
||||
**run with `docker exec <container_name> /app/go-proxy <command>`**
|
||||
|
||||
### Environment variables
|
||||
|
||||
- `GOPROXY_DEBUG`: set to `1` or `true` to enable debug behaviors (i.e. output, etc.)
|
||||
- `GOPROXY_REDIRECT_HTTP`: set to `0` or `false` to disable http to https redirect (only when certs are located)
|
||||
| Environment Variable | Description | Default | Values |
|
||||
| ------------------------------ | ------------------------- | ------- | ------- |
|
||||
| `GOPROXY_NO_SCHEMA_VALIDATION` | disable schema validation | `false` | boolean |
|
||||
| `GOPROXY_DEBUG` | enable debug behaviors | `false` | boolean |
|
||||
|
||||
### Use JSON Schema in VSCode
|
||||
|
||||
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)
|
||||
|
||||
### Config File
|
||||
|
||||
See [config.example.yml](config.example.yml)
|
||||
See [config.example.yml](config.example.yml) for more
|
||||
|
||||
```yaml
|
||||
# autocert configuration
|
||||
autocert:
|
||||
email: # ACME Email
|
||||
domains: # a list of domains for cert registration
|
||||
provider: # DNS Challenge provider
|
||||
options: # provider specific options
|
||||
- ...
|
||||
# reverse proxy providers configuration
|
||||
providers:
|
||||
include:
|
||||
- providers.yml
|
||||
- other_file_1.yml
|
||||
- ...
|
||||
docker:
|
||||
local: $DOCKER_HOST
|
||||
remote-1: tcp://10.0.2.1:2375
|
||||
remote-2: ssh://root:1234@10.0.2.2
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Provider File
|
||||
|
||||
See [providers.example.yml](providers.example.yml)
|
||||
Fields are same as [docker labels](docs/docker.md#labels) starting from `scheme`
|
||||
|
||||
### Supported cert providers
|
||||
See [providers.example.yml](providers.example.yml) for examples
|
||||
|
||||
- Cloudflare
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
...
|
||||
options:
|
||||
auth_token: "YOUR_ZONE_API_TOKEN"
|
||||
```
|
||||
## Known issues
|
||||
|
||||
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
|
||||
- Cert "renewal" is actually obtaining a new cert instead of renewing the existing one
|
||||
|
||||
## Examples
|
||||
|
||||
### Single port configuration example
|
||||
|
||||
```yaml
|
||||
# (default) https://<container_name>.y.z
|
||||
whoami:
|
||||
image: traefik/whoami
|
||||
container_name: whoami # => whoami.y.z
|
||||
|
||||
# enable both subdomain and path matching:
|
||||
whoami:
|
||||
image: traefik/whoami
|
||||
container_name: whoami
|
||||
labels:
|
||||
- proxy.aliases=whoami,apps
|
||||
- proxy.apps.path=/whoami
|
||||
# 1. visit https://whoami.y.z
|
||||
# 2. visit https://apps.y.z/whoami
|
||||
```
|
||||
|
||||
### Multiple ports configuration example
|
||||
|
||||
```yaml
|
||||
minio:
|
||||
image: quay.io/minio/minio
|
||||
container_name: minio
|
||||
...
|
||||
labels:
|
||||
- proxy.aliases=minio,minio-console
|
||||
- proxy.minio.port=9000
|
||||
- proxy.minio-console.port=9001
|
||||
|
||||
# visit https://minio.y.z to access minio
|
||||
# visit https://minio-console.y.z/whoami to access minio console
|
||||
```
|
||||
|
||||
### TCP/UDP configuration example
|
||||
|
||||
```yaml
|
||||
# In the app
|
||||
app-db:
|
||||
image: postgres:15
|
||||
container_name: app-db
|
||||
...
|
||||
labels:
|
||||
# Optional (postgres is in the known image map)
|
||||
- proxy.app-db.scheme=tcp
|
||||
|
||||
# Optional (first free port will be used for listening port)
|
||||
- proxy.app-db.port=20000:postgres
|
||||
|
||||
# In go-proxy
|
||||
go-proxy:
|
||||
...
|
||||
ports:
|
||||
- 80:80
|
||||
...
|
||||
- 20000:20000/tcp
|
||||
# or 20000-20010:20000-20010/tcp to declare large range at once
|
||||
|
||||
# access app-db via <*>.y.z:20000
|
||||
```
|
||||
|
||||
## Load balancing Configuration Example
|
||||
|
||||
```yaml
|
||||
nginx:
|
||||
...
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 3
|
||||
labels:
|
||||
- proxy.nginx.load_balance=1 # allowed: [1, true]
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
```
|
||||
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`
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
## Memory usage
|
||||
|
||||
It takes ~13 MB for 50 proxy entries
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Build it yourself
|
||||
|
||||
1. Install [go](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
|
||||
|
||||
2. get dependencies with `make get`
|
||||
2. Clear cache if you have built this before (go < 1.22) with `go clean -cache`
|
||||
|
||||
3. build binary with `make build`
|
||||
3. get dependencies with `make get`
|
||||
|
||||
4. start your container with `make up` (docker) or `bin/go-proxy` (binary)
|
||||
4. build binary with `make build`
|
||||
|
||||
[panel port]: 8443
|
||||
5. start your container with `make up` (docker) or `bin/go-proxy` (binary)
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
BIN
bin/go-proxy
BIN
bin/go-proxy
Binary file not shown.
@@ -1,51 +1,29 @@
|
||||
version: '3'
|
||||
services:
|
||||
frontend:
|
||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
||||
container_name: go-proxy-frontend
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
labels:
|
||||
- proxy.*.aliases=gp
|
||||
depends_on:
|
||||
- app
|
||||
app:
|
||||
build: .
|
||||
image: ghcr.io/yusing/go-proxy:latest
|
||||
container_name: go-proxy
|
||||
restart: always
|
||||
networks: # ^also add here
|
||||
- default
|
||||
# environment:
|
||||
# - GOPROXY_DEBUG=1 # (optional, enable only for debug)
|
||||
# - GOPROXY_REDIRECT_HTTP=0 # (optional, uncomment to disable http redirect (http -> https))
|
||||
ports:
|
||||
- 80:80 # http
|
||||
# - 443:443 # optional, https
|
||||
- 8080:8080 # http panel
|
||||
# - 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
|
||||
network_mode: host
|
||||
volumes:
|
||||
# use existing certificate
|
||||
# - /path/to/cert.pem:/app/certs/cert.crt:ro
|
||||
# - /path/to/privkey.pem:/app/certs/priv.key:ro
|
||||
|
||||
# use autocert feature
|
||||
# - ./certs:/app/certs
|
||||
|
||||
# if local docker provider is used (by default)
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./config:/app/config
|
||||
|
||||
# to use custom config
|
||||
# - path/to/config.yml:/app/config.yml
|
||||
# (Optional) choose one of below to enable https
|
||||
# 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`
|
||||
|
||||
# mount file provider yaml files
|
||||
# - path/to/provider1.yml:/app/provider1.yml
|
||||
# - path/to/provider2.yml:/app/provider2.yml
|
||||
# etc.
|
||||
dns:
|
||||
- 127.0.0.1 # workaround for "lookup: no such host"
|
||||
extra_hosts:
|
||||
# required if you use local docker provider and have containers in `host` network_mode
|
||||
- host.docker.internal:host-gateway
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-file: '1'
|
||||
max-size: 128k
|
||||
networks: # ^you may add other external networks
|
||||
default:
|
||||
driver: bridge
|
||||
# - /path/to/certs:/app/certs
|
||||
|
||||
# 2. use autocert, certs will be stored in ./certs (or other path you specify)
|
||||
|
||||
# - ./certs:/app/certs
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
# uncomment to use autocert
|
||||
# Autocert (choose one below and uncomment to enable)
|
||||
|
||||
# 1. use existing cert
|
||||
# autocert:
|
||||
# provider: local
|
||||
# cert_path: certs/cert.crt # optional, uncomment only if you need to change it
|
||||
# key_path: certs/priv.key # optional, uncomment only if you need to change it
|
||||
|
||||
# 2. cloudflare
|
||||
# autocert:
|
||||
# email: "user@y.z" # email for acme certificate
|
||||
# domains:
|
||||
# - "*.y.z" # domain for acme certificate, use wild card to allow all subdomains
|
||||
# provider: cloudflare
|
||||
# email: # ACME Email
|
||||
# domains: # a list of domains for cert registration
|
||||
# - x.y.z
|
||||
# options:
|
||||
# auth_token: "YOUR_ZONE_API_TOKEN"
|
||||
# - auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
|
||||
# 3. other providers, check readme for more
|
||||
|
||||
providers:
|
||||
local:
|
||||
kind: docker
|
||||
include:
|
||||
- providers.yml # config/providers.yml
|
||||
# add some more below if you want
|
||||
# - file1.yml # config/file_1.yml
|
||||
# - file2.yml
|
||||
docker:
|
||||
# for value format, see https://docs.docker.com/reference/cli/dockerd/
|
||||
value: FROM_ENV
|
||||
# remote1:
|
||||
# kind: docker
|
||||
# value: ssh://user@10.0.1.1
|
||||
# remote2:
|
||||
# kind: docker
|
||||
# value: tcp://10.0.1.1:2375
|
||||
# provider1:
|
||||
# kind: file
|
||||
# value: provider1.yml
|
||||
# provider2:
|
||||
# kind: file
|
||||
# value: provider2.yml
|
||||
# $DOCKER_HOST implies unix:///var/run/docker.sock by default
|
||||
local: $DOCKER_HOST
|
||||
# add more docker providers if needed
|
||||
# remote-1: tcp://10.0.2.1:2375
|
||||
# remote-2: ssh://root:1234@10.0.2.2
|
||||
|
||||
# Fixed options (optional, non hot-reloadable)
|
||||
|
||||
# timeout_shutdown: 5
|
||||
# redirect_to_https: false
|
||||
|
||||
41
docs/add_dns_provider.md
Normal file
41
docs/add_dns_provider.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Adding provider support
|
||||
|
||||
## **CloudDNS** as an example
|
||||
|
||||
1. Fork this repo, modify [autocert.go](../src/go-proxy/autocert.go#L305)
|
||||
|
||||
```go
|
||||
var providersGenMap = map[string]ProviderGenerator{
|
||||
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
||||
// add here, i.e.
|
||||
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
||||
}
|
||||
```
|
||||
|
||||
2. Go to [https://go-acme.github.io/lego/dns/clouddns](https://go-acme.github.io/lego/dns/clouddns/) and check for required config
|
||||
|
||||
3. Build `go-proxy` with `make build`
|
||||
|
||||
4. Set required config in `config.yml` `autocert` -> `options` section
|
||||
|
||||
```shell
|
||||
# From https://go-acme.github.io/lego/dns/clouddns/
|
||||
CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \
|
||||
CLOUDDNS_EMAIL=you@example.com \
|
||||
CLOUDDNS_PASSWORD=b9841238feb177a84330f \
|
||||
lego --email you@example.com --dns clouddns --domains my.example.org run
|
||||
```
|
||||
|
||||
Should turn into:
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
...
|
||||
options:
|
||||
client_id: bLsdFAks23429841238feb177a572aX
|
||||
email: you@example.com
|
||||
password: b9841238feb177a84330f
|
||||
```
|
||||
|
||||
5. Run with `GOPROXY_NO_SCHEMA_VALIDATION=1` and test if it works
|
||||
6. Commit and create pull request
|
||||
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
|
||||
```
|
||||
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)
|
||||
271
docs/docker.md
Normal file
271
docs/docker.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# Docker compose guide
|
||||
|
||||
## Table of content
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [Docker compose guide](#docker-compose-guide)
|
||||
- [Table of content](#table-of-content)
|
||||
- [Setup](#setup)
|
||||
- [Labels](#labels)
|
||||
- [Syntax](#syntax)
|
||||
- [Fields](#fields)
|
||||
- [Key-value mapping example](#key-value-mapping-example)
|
||||
- [List example](#list-example)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Docker compose examples](#docker-compose-examples)
|
||||
- [Services URLs for above examples](#services-urls-for-above-examples)
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install `wget` if not already
|
||||
|
||||
- Ubuntu based: `sudo apt install -y wget`
|
||||
- Fedora based: `sudo yum install -y wget`
|
||||
- Arch based: `sudo pacman -Sy wget`
|
||||
|
||||
2. Run setup script
|
||||
|
||||
`bash <(wget -qO- https://github.com/yusing/go-proxy/raw/main/setup-docker.sh)`
|
||||
|
||||
It will setup folder structure and required config files
|
||||
|
||||
3. Verify folder structure and then `cd go-proxy`
|
||||
|
||||
```plain
|
||||
go-proxy
|
||||
├── certs
|
||||
├── compose.yml
|
||||
└── config
|
||||
├── config.yml
|
||||
└── providers.yml
|
||||
```
|
||||
|
||||
4. Enable HTTPs _(optional)_
|
||||
|
||||
Mount a folder (to store obtained certs) or (containing existing cert)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
go-proxy:
|
||||
...
|
||||
volumes:
|
||||
- ./certs:/app/certs
|
||||
```
|
||||
|
||||
To use **autocert**, complete that section in `config.yml`, e.g.
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
email: john.doe@x.y.z # ACME Email
|
||||
domains: # a list of domains for cert registration
|
||||
- x.y.z
|
||||
provider: cloudflare
|
||||
options:
|
||||
- auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
```
|
||||
|
||||
To use **existing certificate**, set path for cert and key in `config.yml`, e.g.
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
cert_path: /app/certs/cert.crt
|
||||
key_path: /app/certs/priv.key
|
||||
```
|
||||
|
||||
5. Modify `compose.yml` to fit your needs
|
||||
|
||||
6. Run `docker compose up -d` to start the container
|
||||
|
||||
7. Navigate to Web panel `http://gp.yourdomain.com` or use **Visual Studio Code (provides schema check)** to edit proxy config
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Labels
|
||||
|
||||
### Syntax
|
||||
|
||||
| Label | Description |
|
||||
| ----------------------- | -------------------------------------------------------- |
|
||||
| `proxy.aliases` | comma separated aliases for subdomain and label matching |
|
||||
| `proxy.<alias>.<field>` | set field for specific alias |
|
||||
| `proxy.*.<field>` | set field for all aliases |
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Description | Default | Allowed Values / Syntax |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `scheme` | proxy protocol | <ul><li>`http` for numeric port</li><li>`tcp` for `x:y` port</li></ul> | `http`, `https`, `tcp`, `udp` |
|
||||
| `host` | proxy host | <ul><li>Docker: `container_name`</li><li>File: `localhost`</li></ul> | IP address, hostname |
|
||||
| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `1 - 65535` |
|
||||
| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y` <br><ul><li>x: port for `go-proxy` to listen on</li><li>y: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> |
|
||||
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
||||
| `path_patterns` | proxy path patterns **(http/s only)**<br> only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[<sup>1</sup>](#list-example) of path patterns ([syntax](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) |
|
||||
| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[<sup>2</sup>](#key-value-mapping-example) of header-value pairs |
|
||||
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>1</sup>](#list-example) of headers |
|
||||
|
||||
#### Key-value mapping example
|
||||
|
||||
Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx:
|
||||
...
|
||||
labels:
|
||||
# values from duplicated header keys will be combined
|
||||
proxy.nginx.set_headers: | # remember to add the '|'
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
X-Custom-Header2: value4
|
||||
# X-Custom-Header2 will be "value3, value4"
|
||||
```
|
||||
|
||||
File Provider
|
||||
|
||||
```yaml
|
||||
service_a:
|
||||
host: service_a.internal
|
||||
set_headers:
|
||||
# do not duplicate header keys, as it is not allowed in YAML
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
```
|
||||
|
||||
#### List example
|
||||
|
||||
Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx:
|
||||
...
|
||||
labels:
|
||||
proxy.nginx.path_patterns: | # remember to add the '|'
|
||||
- GET /
|
||||
- POST /auth
|
||||
proxy.nginx.hide_headers: | # remember to add the '|'
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
```
|
||||
|
||||
File Provider
|
||||
|
||||
```yaml
|
||||
service_a:
|
||||
host: service_a.internal
|
||||
path_patterns:
|
||||
- GET /
|
||||
- POST /auth
|
||||
hide_headers:
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Firewall issues
|
||||
|
||||
If you are using `ufw` with vpn that drop all inbound traffic except vpn, run below:
|
||||
|
||||
`sudo ufw allow from 172.16.0.0/16 to 100.64.0.0/10`
|
||||
|
||||
Explaination:
|
||||
|
||||
Docker network is usually `172.16.0.0/16`
|
||||
|
||||
Tailscale is used as an example, `100.64.0.0/10` will be the CIDR
|
||||
|
||||
You can also list CIDRs of all docker bridge networks by:
|
||||
|
||||
`docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -`
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Docker compose examples
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
adg-work:
|
||||
adg-conf:
|
||||
mc-data:
|
||||
palworld:
|
||||
nginx:
|
||||
services:
|
||||
adg:
|
||||
image: adguard/adguardhome
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- proxy.aliases=adg,adg-dns,adg-setup
|
||||
- proxy.adg.port=80
|
||||
- proxy.adg-setup.port=3000
|
||||
- proxy.adg-dns.scheme=udp
|
||||
- proxy.adg-dns.port=20000:dns
|
||||
volumes:
|
||||
- adg-work:/opt/adguardhome/work
|
||||
- adg-conf:/opt/adguardhome/conf
|
||||
mc:
|
||||
image: itzg/minecraft-server
|
||||
tty: true
|
||||
stdin_open: true
|
||||
container_name: mc
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- proxy.mc.scheme=tcp
|
||||
- proxy.mc.port=20001:25565
|
||||
environment:
|
||||
- EULA=TRUE
|
||||
volumes:
|
||||
- mc-data:/data
|
||||
palworld:
|
||||
image: thijsvanloef/palworld-server-docker:latest
|
||||
restart: unless-stopped
|
||||
container_name: pal
|
||||
stop_grace_period: 30s
|
||||
labels:
|
||||
- proxy.aliases=pal1,pal2
|
||||
- proxy.*.scheme=udp
|
||||
- proxy.pal1.port=20002:8211
|
||||
- proxy.pal2.port=20003:27015
|
||||
environment: ...
|
||||
volumes:
|
||||
- palworld:/palworld
|
||||
nginx:
|
||||
image: nginx
|
||||
container_name: nginx
|
||||
volumes:
|
||||
- nginx:/usr/share/nginx/html
|
||||
go-proxy:
|
||||
image: ghcr.io/yusing/go-proxy:latest
|
||||
container_name: go-proxy
|
||||
restart: always
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
go-proxy-frontend:
|
||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
||||
container_name: go-proxy-frontend
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
labels:
|
||||
- proxy.*.aliases=gp
|
||||
depends_on:
|
||||
- go-proxy
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Services URLs for above examples
|
||||
|
||||
- `gp.yourdomain.com`: go-proxy web panel
|
||||
- `adg-setup.yourdomain.com`: adguard setup (first time setup)
|
||||
- `adg.yourdomain.com`: adguard dashboard
|
||||
- `nginx.yourdomain.com`: nginx
|
||||
- `yourdomain.com:53`: adguard dns
|
||||
- `yourdomain.com:25565`: minecraft server
|
||||
- `yourdomain.com:8211`: palworld server
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
if [ "$1" == "restart" ]; then
|
||||
echo "restarting"
|
||||
killall go-proxy
|
||||
fi
|
||||
if [ "$GOPROXY_DEBUG" == "1" ]; then
|
||||
/app/go-proxy 2> log/go-proxy.log &
|
||||
tail -f /dev/null
|
||||
else
|
||||
/app/go-proxy
|
||||
fi
|
||||
1
frontend
Submodule
1
frontend
Submodule
Submodule frontend added at 8cdf9eaa10
52
go.mod
52
go.mod
@@ -1,52 +0,0 @@
|
||||
module github.com/yusing/go-proxy
|
||||
|
||||
go 1.21.7
|
||||
|
||||
require (
|
||||
github.com/docker/cli v26.0.0+incompatible
|
||||
github.com/docker/docker v26.0.0+incompatible
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/go-acme/lego/v4 v4.16.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
golang.org/x/net v0.22.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.91.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||
github.com/miekg/dns v1.1.58 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
@@ -1,15 +1,22 @@
|
||||
app: # matching `app.y.z`
|
||||
# optional
|
||||
scheme: http
|
||||
# required, proxy target
|
||||
example: # matching `app.y.z`
|
||||
scheme: https
|
||||
host: 10.0.0.1
|
||||
# optional
|
||||
port: 80
|
||||
# optional, defaults to empty
|
||||
path:
|
||||
# optional
|
||||
path_mode:
|
||||
# optional
|
||||
notlsverify: false
|
||||
# app2:
|
||||
# ...
|
||||
path_patterns: # Check https://pkg.go.dev/net/http#hdr-Patterns-ServeMux for syntax
|
||||
- GET / # accept any GET request
|
||||
- POST /auth # for /auth and /auth/* accept only POST
|
||||
- GET /home/{$}
|
||||
- /b/{bucket}/o/{any}
|
||||
no_tls_verify: false
|
||||
set_headers:
|
||||
HEADER_A: VALUE_A, VALUE_B
|
||||
HEADER_B: VALUE_C
|
||||
hide_headers:
|
||||
- HEADER_C
|
||||
- HEADER_D
|
||||
app1:
|
||||
host: some_host
|
||||
app2:
|
||||
scheme: tcp
|
||||
host: 10.0.0.2
|
||||
port: 20000:tcp
|
||||
|
||||
207
schema/config.schema.json
Normal file
207
schema/config.schema.json
Normal file
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "go-proxy config file",
|
||||
"properties": {
|
||||
"autocert": {
|
||||
"title": "Autocert configuration",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"title": "ACME Email",
|
||||
"type": "string",
|
||||
"pattern": "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
|
||||
"patternErrorMessage": "Invalid email"
|
||||
},
|
||||
"domains": {
|
||||
"title": "Cert Domains",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"cert_path": {
|
||||
"title": "path of cert file to load/store",
|
||||
"default": "certs/cert.crt",
|
||||
"markdownDescription": "default: `certs/cert.crt`",
|
||||
"type": "string"
|
||||
},
|
||||
"key_path": {
|
||||
"title": "path of key file to load/store",
|
||||
"default": "certs/priv.key",
|
||||
"markdownDescription": "default: `certs/priv.key`",
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"title": "DNS Challenge Provider",
|
||||
"default": "local",
|
||||
"type": "string",
|
||||
"enum": ["local", "cloudflare", "clouddns", "duckdns"]
|
||||
},
|
||||
"options": {
|
||||
"title": "Provider specific options",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"not": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "local"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["email", "domains", "provider", "options"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "cloudflare"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"options": {
|
||||
"required": ["auth_token"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"auth_token": {
|
||||
"description": "Cloudflare API Token with Zone Scope",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "clouddns"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"options": {
|
||||
"required": ["client_id", "email", "password"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"client_id": {
|
||||
"description": "CloudDNS Client ID",
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"description": "CloudDNS Email",
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"description": "CloudDNS Password",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "duckdns"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"options": {
|
||||
"required": ["token"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"token": {
|
||||
"description": "DuckDNS Token",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"providers": {
|
||||
"title": "Proxy providers configuration",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"include": {
|
||||
"title": "Proxy providers configuration files",
|
||||
"description": "relative path to 'config'",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9_-]+\\.(yml|yaml)$",
|
||||
"patternErrorMessage": "Invalid file name"
|
||||
}
|
||||
},
|
||||
"docker": {
|
||||
"title": "Docker provider configuration",
|
||||
"description": "docker clients (name-address pairs)",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9-_]+$": {
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"unix:///var/run/docker.sock",
|
||||
"tcp://127.0.0.1:2375",
|
||||
"ssh://user@host:port"
|
||||
],
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "$DOCKER_HOST",
|
||||
"description": "Use DOCKER_HOST environment variable"
|
||||
},
|
||||
{
|
||||
"pattern": "^unix://.+$",
|
||||
"description": "A Unix socket for local Docker communication."
|
||||
},
|
||||
{
|
||||
"pattern": "^ssh://.+$",
|
||||
"description": "An SSH connection to a remote Docker host."
|
||||
},
|
||||
{
|
||||
"pattern": "^fd://.+$",
|
||||
"description": "A file descriptor for Docker communication."
|
||||
},
|
||||
{
|
||||
"pattern": "^tcp://.+$",
|
||||
"description": "A TCP connection to a remote Docker host."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeout_shutdown": {
|
||||
"title": "Shutdown timeout (in seconds)",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"redirect_to_https": {
|
||||
"title": "Redirect to HTTPS on HTTP requests",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["providers"]
|
||||
}
|
||||
195
schema/providers.schema.json
Normal file
195
schema/providers.schema.json
Normal file
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "go-proxy providers file",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9_-]+$": {
|
||||
"title": "Proxy entry",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"title": "Proxy scheme (http, https, tcp, udp)",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"tcp",
|
||||
"udp",
|
||||
"tcp:tcp",
|
||||
"udp:udp",
|
||||
"tcp:udp",
|
||||
"udp:tcp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
"description": "Auto detect base on port format"
|
||||
}
|
||||
]
|
||||
},
|
||||
"host": {
|
||||
"default": "localhost",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null",
|
||||
"description": "localhost (default)"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "ipv4",
|
||||
"description": "Proxy to ipv4 address"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "ipv6",
|
||||
"description": "Proxy to ipv6 address"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "hostname",
|
||||
"description": "Proxy to hostname"
|
||||
}
|
||||
],
|
||||
"title": "Proxy host (ipv4 / ipv6 / hostname)"
|
||||
},
|
||||
"port": {},
|
||||
"no_tls_verify": {},
|
||||
"path_patterns": {},
|
||||
"set_headers": {},
|
||||
"hide_headers": {}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": ["http", "https"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"port": {
|
||||
"markdownDescription": "Proxy port from **1** to **65535**",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^\\d{1,5}$",
|
||||
"patternErrorMessage": "`port` must be a number"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
}
|
||||
]
|
||||
},
|
||||
"path_patterns": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"markdownDescription": "A list of [path patterns](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^((GET|POST|DELETE|PUT|PATCH|HEAD|OPTIONS|CONNECT)\\s)?(/(\\w*|{\\w*}|{\\$}))+/?$",
|
||||
"patternErrorMessage": "invalid path pattern"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
"description": "No proxy path"
|
||||
}
|
||||
]
|
||||
},
|
||||
"set_headers": {
|
||||
"type": "object",
|
||||
"description": "Proxy headers to set",
|
||||
"additionalProperties": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"hide_headers": {
|
||||
"type": "array",
|
||||
"description": "Proxy headers to hide",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"properties": {
|
||||
"port": {
|
||||
"markdownDescription": "`listening port:proxy port` or `listening port:service name`",
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+\\:[0-9a-z]+$",
|
||||
"patternErrorMessage": "invalid syntax"
|
||||
},
|
||||
"no_tls_verify": {
|
||||
"not": true
|
||||
},
|
||||
"path_patterns": {
|
||||
"not": true
|
||||
},
|
||||
"set_headers": {
|
||||
"not": true
|
||||
},
|
||||
"hide_headers": {
|
||||
"not": true
|
||||
}
|
||||
},
|
||||
"required": ["port"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"const": "https"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"no_tls_verify": {
|
||||
"description": "Disable TLS verification for https proxy",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"properties": {
|
||||
"no_tls_verify": {
|
||||
"not": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 149 KiB |
14
setup-docker.sh
Normal file
14
setup-docker.sh
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
if [ -z "$BRANCH" ]; then
|
||||
BRANCH="main"
|
||||
fi
|
||||
BASE_URL="https://github.com/yusing/go-proxy/raw/${BRANCH}"
|
||||
mkdir -p go-proxy
|
||||
cd go-proxy
|
||||
mkdir -p config
|
||||
mkdir -p certs
|
||||
[ -f compose.yml ] || wget -cO - ${BASE_URL}/compose.example.yml > compose.yml
|
||||
[ -f config/config.yml ] || wget -cO - ${BASE_URL}/config.example.yml > config/config.yml
|
||||
[ -f config/providers.yml ] || touch config/providers.yml
|
||||
30
src/api/handler.go
Normal file
30
src/api/handler.go
Normal file
@@ -0,0 +1,30 @@
|
||||
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("POST /v1/file/{filename}", v1.SetFileContent)
|
||||
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)
|
||||
}
|
||||
}
|
||||
39
src/api/v1/checkhealth.go
Normal file
39
src/api/v1/checkhealth.go
Normal file
@@ -0,0 +1,39 @@
|
||||
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:
|
||||
ok = U.IsSiteHealthy(route.TargetURL.String())
|
||||
case *R.StreamRoute:
|
||||
ok = U.IsStreamHealthy(
|
||||
string(route.Scheme.ProxyScheme),
|
||||
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 (
|
||||
"io"
|
||||
"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, err := E.Check(io.ReadAll(r.Body))
|
||||
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, http.StatusBadRequest)
|
||||
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("module", "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()
|
||||
}
|
||||
31
src/autocert/constants.go
Normal file
31
src/autocert/constants.go
Normal file
@@ -0,0 +1,31 @@
|
||||
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{
|
||||
"": providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
||||
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("module", "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("module", "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
|
||||
}
|
||||
39
src/common/args.go
Normal file
39
src/common/args.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
Command string
|
||||
}
|
||||
|
||||
const (
|
||||
CommandStart = ""
|
||||
CommandValidate = "validate"
|
||||
CommandReload = "reload"
|
||||
)
|
||||
|
||||
var ValidCommands = []string{CommandStart, CommandValidate, CommandReload}
|
||||
|
||||
func GetArgs() Args {
|
||||
var args Args
|
||||
flag.Parse()
|
||||
args.Command = flag.Arg(0)
|
||||
if err := validateArgs(args.Command, ValidCommands); err.IsNotNil() {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func validateArgs[T comparable](arg T, validArgs []T) E.NestedError {
|
||||
for _, v := range validArgs {
|
||||
if arg == v {
|
||||
return E.Nil()
|
||||
}
|
||||
}
|
||||
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 = "$DOCKER_HOST"
|
||||
|
||||
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,
|
||||
}
|
||||
23
src/common/env.go
Normal file
23
src/common/env.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
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"
|
||||
}
|
||||
257
src/config/config.go
Normal file
257
src/config/config.go
Normal file
@@ -0,0 +1,257 @@
|
||||
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("module", "config"),
|
||||
reader: U.NewFileReader(common.ConfigPath),
|
||||
watcher: W.NewFileWatcher(common.ConfigFileName),
|
||||
reloadReq: make(chan struct{}, 1),
|
||||
}
|
||||
if err := cfg.load(); err.IsNotNil() {
|
||||
return nil, err
|
||||
}
|
||||
cfg.startProviders()
|
||||
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["type"] = p.GetType()
|
||||
stats["num_streams"] = nStreams
|
||||
stats["num_reverse_proxies"] = nRPs
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
warnings := E.NewBuilder("errors loading config")
|
||||
|
||||
cfg.l.Debug("starting autocert")
|
||||
ap, err := autocert.NewConfig(&model.AutoCert).GetProvider()
|
||||
if err.IsNotNil() {
|
||||
warnings.Add(E.Failure("autocert provider").With(err))
|
||||
} else {
|
||||
cfg.l.Debug("started autocert")
|
||||
}
|
||||
cfg.autocertProvider = ap
|
||||
|
||||
cfg.l.Debug("loading providers")
|
||||
cfg.proxyProviders = F.NewMap[string, *PR.Provider]()
|
||||
for _, filename := range model.Providers.Files {
|
||||
p := PR.NewFileProvider(filename)
|
||||
cfg.proxyProviders.Set(p.GetName(), p)
|
||||
}
|
||||
for name, dockerHost := range model.Providers.Docker {
|
||||
p := PR.NewDockerProvider(name, dockerHost)
|
||||
cfg.proxyProviders.Set(p.GetName(), p)
|
||||
}
|
||||
cfg.l.Debug("loaded providers")
|
||||
|
||||
cfg.value = model
|
||||
|
||||
if err := warnings.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).Subject(p))
|
||||
}
|
||||
})
|
||||
|
||||
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 routes", (*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("module", "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"
|
||||
78
src/docker/label.go
Normal file
78
src/docker/label.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"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)
|
||||
70
src/docker/label_parser.go
Normal file
70
src/docker/label_parser.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func yamlListParser(value string) (any, E.NestedError) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return []string{}, E.Nil()
|
||||
}
|
||||
var data []string
|
||||
err := E.From(yaml.Unmarshal([]byte(value), &data))
|
||||
return data, err
|
||||
}
|
||||
|
||||
func yamlStringMappingParser(value string) (any, E.NestedError) {
|
||||
value = strings.TrimSpace(value)
|
||||
lines := strings.Split(value, "\n")
|
||||
h := make(map[string]string)
|
||||
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])
|
||||
if existing, ok := h[key]; ok {
|
||||
h[key] = existing + ", " + val
|
||||
} else {
|
||||
h[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,
|
||||
"path_patterns": yamlListParser,
|
||||
"set_headers": yamlStringMappingParser,
|
||||
"hide_headers": yamlListParser,
|
||||
"no_tls_verify": boolParser,
|
||||
})
|
||||
return 0
|
||||
}()
|
||||
169
src/docker/label_parser_test.go
Normal file
169
src/docker/label_parser_test.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"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 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.Error())
|
||||
}
|
||||
if pl.Target != alias {
|
||||
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||
}
|
||||
if pl.Attribute != field {
|
||||
t.Errorf("expected field=%s, got %s", field, pl.Target)
|
||||
}
|
||||
if pl.Value != v {
|
||||
t.Errorf("expected value=%q, got %s", v, pl.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringProxyLabel(t *testing.T) {
|
||||
v := "bar"
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "ip"), v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
if pl.Value != v {
|
||||
t.Errorf("expected value=%q, got %s", v, pl.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoolProxyLabelValid(t *testing.T) {
|
||||
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, "foo", "no_tls_verify"), k)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
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 !err.Is(E.ErrInvalid) {
|
||||
t.Errorf("expected err InvalidProxyLabel, got %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetHeaderProxyLabelValid(t *testing.T) {
|
||||
v := `
|
||||
X-Custom-Header1: foo, bar
|
||||
X-Custom-Header1: baz
|
||||
X-Custom-Header2: boo`
|
||||
v = strings.TrimPrefix(v, "\n")
|
||||
h := map[string]string{
|
||||
"X-Custom-Header1": "foo, bar, baz",
|
||||
"X-Custom-Header2": "boo",
|
||||
}
|
||||
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
hGot, ok := pl.Value.(map[string]string)
|
||||
if !ok {
|
||||
t.Errorf("value is not a map[string]string, but %T", pl.Value)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(h, hGot) {
|
||||
t.Errorf("expected %v, got %v", h, hGot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetHeaderProxyLabelInvalid(t *testing.T) {
|
||||
tests := []string{
|
||||
"X-Custom-Header1 = bar",
|
||||
"X-Custom-Header1",
|
||||
"- X-Custom-Header1",
|
||||
}
|
||||
|
||||
for _, v := range tests {
|
||||
_, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v)
|
||||
if !err.Is(E.ErrInvalid) {
|
||||
t.Errorf("expected invalid err for %q, got %s", v, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHideHeadersProxyLabel(t *testing.T) {
|
||||
v := `
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
- X-Custom-Header3
|
||||
`
|
||||
v = strings.TrimPrefix(v, "\n")
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "hide_headers"), v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
sGot, ok := pl.Value.([]string)
|
||||
sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
|
||||
if !ok {
|
||||
t.Errorf("value is not []string, but %T", pl.Value)
|
||||
}
|
||||
if !reflect.DeepEqual(sGot, sWant) {
|
||||
t.Errorf("expected %q, got %q", sWant, sGot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommaSepProxyLabelSingle(t *testing.T) {
|
||||
v := "a"
|
||||
pl, err := ParseLabel("proxy.aliases", v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
sGot, ok := pl.Value.([]string)
|
||||
sWant := []string{"a"}
|
||||
if !ok {
|
||||
t.Errorf("value is not []string, but %T", pl.Value)
|
||||
}
|
||||
if !reflect.DeepEqual(sGot, sWant) {
|
||||
t.Errorf("expected %q, got %q", sWant, sGot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommaSepProxyLabelMulti(t *testing.T) {
|
||||
v := "X-Custom-Header1, X-Custom-Header2,X-Custom-Header3"
|
||||
pl, err := ParseLabel("proxy.aliases", v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
sGot, ok := pl.Value.([]string)
|
||||
sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
|
||||
if !ok {
|
||||
t.Errorf("value is not []string, but %T", pl.Value)
|
||||
}
|
||||
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...)
|
||||
}
|
||||
28
src/error/builder_test.go
Normal file
28
src/error/builder_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package error
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuilder(t *testing.T) {
|
||||
eb := NewBuilder("error occurred")
|
||||
eb.Add(Failure("Action 1").With(Invalid("Inner", "1")).With(Invalid("Inner", "2")))
|
||||
eb.Add(Failure("Action 2").With(Invalid("Inner", "3")))
|
||||
|
||||
got := eb.Build().Error()
|
||||
expected1 :=
|
||||
(`error occurred:
|
||||
- Action 1 failed:
|
||||
- invalid Inner - 1
|
||||
- invalid Inner - 2
|
||||
- Action 2 failed:
|
||||
- invalid Inner - 3`)
|
||||
expected2 :=
|
||||
(`error occurred:
|
||||
- Action 1 failed:
|
||||
- invalid Inner - 2
|
||||
- invalid Inner - 1
|
||||
- Action 2 failed:
|
||||
- invalid Inner - 3`)
|
||||
if got != expected1 && got != expected2 {
|
||||
t.Errorf("expected \n%s, got \n%s", expected1, got)
|
||||
}
|
||||
}
|
||||
168
src/error/error.go
Normal file
168
src/error/error.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package error
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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 {
|
||||
subject string
|
||||
err error // can be nil
|
||||
extras []NestedError
|
||||
}
|
||||
)
|
||||
|
||||
func Nil() NestedError { return NestedError{} }
|
||||
|
||||
func From(err error) NestedError {
|
||||
switch err := err.(type) {
|
||||
case nil:
|
||||
return Nil()
|
||||
case NestedError:
|
||||
return err
|
||||
default:
|
||||
return NestedError{err: 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([]NestedError, 0, len(err))
|
||||
nErr := 0
|
||||
for _, e := range err {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
extras = append(extras, From(e))
|
||||
nErr += 1
|
||||
}
|
||||
if nErr == 0 {
|
||||
return Nil()
|
||||
}
|
||||
return NestedError{
|
||||
err: errors.New(message),
|
||||
extras: extras,
|
||||
}
|
||||
}
|
||||
|
||||
func (ne NestedError) Error() string {
|
||||
var buf strings.Builder
|
||||
ne.writeToSB(&buf, 0, "")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (ne NestedError) Is(err error) bool {
|
||||
return errors.Is(ne.err, err)
|
||||
}
|
||||
|
||||
func (ne NestedError) With(s any) NestedError {
|
||||
var msg string
|
||||
switch ss := s.(type) {
|
||||
case nil:
|
||||
return ne
|
||||
case error:
|
||||
return ne.withError(ss)
|
||||
case string:
|
||||
msg = ss
|
||||
case fmt.Stringer:
|
||||
msg = ss.String()
|
||||
default:
|
||||
msg = fmt.Sprint(s)
|
||||
}
|
||||
return ne.withError(errors.New(msg))
|
||||
}
|
||||
|
||||
func (ne NestedError) Extraf(format string, args ...any) NestedError {
|
||||
return ne.With(fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
func (ne NestedError) Subject(s any) NestedError {
|
||||
switch ss := s.(type) {
|
||||
case string:
|
||||
ne.subject = ss
|
||||
case fmt.Stringer:
|
||||
ne.subject = ss.String()
|
||||
default:
|
||||
ne.subject = fmt.Sprint(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")
|
||||
}
|
||||
ne.subject = fmt.Sprintf(format, args...)
|
||||
return ne
|
||||
}
|
||||
|
||||
func (ne NestedError) IsNil() bool {
|
||||
return ne.err == nil
|
||||
}
|
||||
|
||||
func (ne NestedError) IsNotNil() bool {
|
||||
return ne.err != nil
|
||||
}
|
||||
|
||||
func errorf(format string, args ...any) NestedError {
|
||||
return From(fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
func (ne NestedError) withError(err error) NestedError {
|
||||
ne.extras = append(ne.extras, From(err))
|
||||
return ne
|
||||
}
|
||||
|
||||
func (ne *NestedError) writeToSB(sb *strings.Builder, level int, prefix string) {
|
||||
ne.writeIndents(sb, level)
|
||||
sb.WriteString(prefix)
|
||||
|
||||
if ne.IsNil() {
|
||||
sb.WriteString("nil")
|
||||
return
|
||||
}
|
||||
|
||||
sb.WriteString(ne.err.Error())
|
||||
if ne.subject != "" {
|
||||
if ne.err != nil {
|
||||
sb.WriteString(fmt.Sprintf(" for %q", ne.subject))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprint(ne.subject))
|
||||
}
|
||||
}
|
||||
if len(ne.extras) > 0 {
|
||||
sb.WriteRune(':')
|
||||
for _, extra := range ne.extras {
|
||||
sb.WriteRune('\n')
|
||||
extra.writeToSB(sb, level+1, "- ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ne *NestedError) writeIndents(sb *strings.Builder, level int) {
|
||||
for i := 0; i < level; i++ {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
}
|
||||
80
src/error/error_test.go
Normal file
80
src/error/error_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package error
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func AssertEq[T comparable](t *testing.T, got, want T) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("expected:\n%v, got\n%v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorIs(t *testing.T) {
|
||||
AssertEq(t, Failure("foo").Is(ErrFailure), true)
|
||||
AssertEq(t, Failure("foo").With("bar").Is(ErrFailure), true)
|
||||
AssertEq(t, Failure("foo").With("bar").Is(ErrInvalid), false)
|
||||
AssertEq(t, Failure("foo").With("bar").With("baz").Is(ErrInvalid), false)
|
||||
|
||||
AssertEq(t, Invalid("foo", "bar").Is(ErrInvalid), true)
|
||||
AssertEq(t, Invalid("foo", "bar").Is(ErrFailure), false)
|
||||
|
||||
AssertEq(t, Nil().Is(nil), true)
|
||||
AssertEq(t, Nil().Is(ErrInvalid), false)
|
||||
AssertEq(t, Invalid("foo", "bar").Is(nil), false)
|
||||
}
|
||||
|
||||
func TestNil(t *testing.T) {
|
||||
AssertEq(t, Nil().IsNil(), true)
|
||||
AssertEq(t, Nil().IsNotNil(), false)
|
||||
AssertEq(t, Nil().Error(), "nil")
|
||||
}
|
||||
|
||||
func TestErrorSimple(t *testing.T) {
|
||||
ne := Failure("foo bar")
|
||||
AssertEq(t, ne.Error(), "foo bar failed")
|
||||
ne = ne.Subject("baz")
|
||||
AssertEq(t, ne.Error(), "foo bar failed for \"baz\"")
|
||||
}
|
||||
|
||||
func TestErrorWith(t *testing.T) {
|
||||
ne := Failure("foo").With("bar").With("baz")
|
||||
AssertEq(t, ne.Error(), "foo failed:\n - bar\n - baz")
|
||||
}
|
||||
|
||||
func TestErrorNested(t *testing.T) {
|
||||
inner := Failure("inner").
|
||||
With("1").
|
||||
With("1")
|
||||
inner2 := Failure("inner2").
|
||||
Subject("action 2").
|
||||
With("2").
|
||||
With("2")
|
||||
inner3 := Failure("inner3").
|
||||
Subject("action 3").
|
||||
With("3").
|
||||
With("3")
|
||||
ne := Failure("foo").
|
||||
With("bar").
|
||||
With("baz").
|
||||
With(inner).
|
||||
With(inner.With(inner2.With(inner3)))
|
||||
want :=
|
||||
`foo failed:
|
||||
- bar
|
||||
- baz
|
||||
- inner failed:
|
||||
- 1
|
||||
- 1
|
||||
- inner failed:
|
||||
- 1
|
||||
- 1
|
||||
- inner2 failed for "action 2":
|
||||
- 2
|
||||
- 2
|
||||
- inner3 failed for "action 3":
|
||||
- 3
|
||||
- 3`
|
||||
AssertEq(t, ne.Error(), want)
|
||||
}
|
||||
33
src/error/errors.go
Normal file
33
src/error/errors.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package error
|
||||
|
||||
import (
|
||||
stderrors "errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailure = stderrors.New("failed")
|
||||
ErrInvalid = stderrors.New("invalid")
|
||||
ErrUnsupported = stderrors.New("unsupported")
|
||||
ErrNotExists = stderrors.New("does not exist")
|
||||
ErrDuplicated = stderrors.New("duplicated")
|
||||
)
|
||||
|
||||
func Failure(what string) NestedError {
|
||||
return errorf("%s %w", what, ErrFailure)
|
||||
}
|
||||
|
||||
func Invalid(subject, what any) NestedError {
|
||||
return errorf("%w %v - %v", ErrInvalid, subject, what)
|
||||
}
|
||||
|
||||
func Unsupported(subject, what any) NestedError {
|
||||
return errorf("%w %v - %v", ErrUnsupported, subject, what)
|
||||
}
|
||||
|
||||
func NotExists(subject, what any) NestedError {
|
||||
return errorf("%s %v - %v", subject, ErrNotExists, what)
|
||||
}
|
||||
|
||||
func Duplicated(subject, what any) NestedError {
|
||||
return errorf("%w %v: %v", ErrDuplicated, subject, what)
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"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/cloudflare"
|
||||
"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
|
||||
Domains []string `yaml:",flow"`
|
||||
Provider string
|
||||
Options ProviderOptions `yaml:",flow"`
|
||||
}
|
||||
|
||||
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() error
|
||||
RenewalOn() time.Time
|
||||
ScheduleRenewal()
|
||||
}
|
||||
|
||||
func (cfg AutoCertConfig) GetProvider() (AutoCertProvider, error) {
|
||||
if len(cfg.Domains) == 0 {
|
||||
return nil, fmt.Errorf("no domains specified")
|
||||
}
|
||||
if cfg.Provider == "" {
|
||||
return nil, fmt.Errorf("no provider specified")
|
||||
}
|
||||
if cfg.Email == "" {
|
||||
return nil, fmt.Errorf("no email specified")
|
||||
}
|
||||
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to generate private key: %v", 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, fmt.Errorf("unable to create lego client: %v", err)
|
||||
}
|
||||
base := &autoCertProvider{
|
||||
name: cfg.Provider,
|
||||
cfg: cfg,
|
||||
user: user,
|
||||
legoCfg: legoCfg,
|
||||
client: legoClient,
|
||||
}
|
||||
gen, ok := providersGenMap[cfg.Provider]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown provider: %s", cfg.Provider)
|
||||
}
|
||||
legoProvider, err := gen(cfg.Options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create provider: %v", err)
|
||||
}
|
||||
err = legoClient.Challenge.SetDNS01Provider(legoProvider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to set challenge provider: %v", 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 {
|
||||
aclog.Fatal("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() error {
|
||||
client := p.client
|
||||
if p.user.Registration == nil {
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.user.Registration = reg
|
||||
}
|
||||
req := certificate.ObtainRequest{
|
||||
Domains: p.cfg.Domains,
|
||||
Bundle: true,
|
||||
}
|
||||
cert, err := client.Certificate.Obtain(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.saveCert(cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expiries, err := getCertExpiries(&tlsCert)
|
||||
if err != nil {
|
||||
return 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) RenewalOn() time.Time {
|
||||
t := time.Now().AddDate(0, 0, 3)
|
||||
for _, expiry := range p.certExpiries {
|
||||
if expiry.Before(t) {
|
||||
return time.Now()
|
||||
}
|
||||
return t
|
||||
}
|
||||
// this line should never be reached
|
||||
panic("no certificate available")
|
||||
}
|
||||
|
||||
func (p *autoCertProvider) ScheduleRenewal() {
|
||||
for {
|
||||
t := time.Until(p.RenewalOn())
|
||||
aclog.Infof("next renewal in %v", t)
|
||||
time.Sleep(t)
|
||||
err := p.renewIfNeeded()
|
||||
if err != nil {
|
||||
aclog.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *autoCertProvider) saveCert(cert *certificate.Resource) error {
|
||||
err := os.MkdirAll(path.Dir(certFileDefault), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create cert directory: %v", err)
|
||||
}
|
||||
err = os.WriteFile(keyFileDefault, cert.PrivateKey, 0600) // -rw-------
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write key file: %v", err)
|
||||
}
|
||||
err = os.WriteFile(certFileDefault, cert.Certificate, 0644) // -rw-r--r--
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write cert file: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *autoCertProvider) needRenewal() bool {
|
||||
return time.Now().After(p.RenewalOn())
|
||||
}
|
||||
|
||||
func (p *autoCertProvider) renewIfNeeded() error {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
trials++
|
||||
if trials > 3 {
|
||||
return fmt.Errorf("unable to renew certificate: %v after 3 trials", err)
|
||||
}
|
||||
aclog.Errorf("failed to renew certificate: %v, trying again in 5 seconds", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func providerGenerator[CT interface{}, 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, 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 err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var providersGenMap = map[string]ProviderGenerator{
|
||||
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// commented out if unused
|
||||
type Config interface {
|
||||
// Load() error
|
||||
MustLoad()
|
||||
GetAutoCertProvider() (AutoCertProvider, error)
|
||||
// MustReload()
|
||||
// Reload() error
|
||||
StartProviders()
|
||||
StopProviders()
|
||||
WatchChanges()
|
||||
StopWatching()
|
||||
}
|
||||
|
||||
func NewConfig() Config {
|
||||
cfg := &config{}
|
||||
cfg.watcher = NewFileWatcher(
|
||||
configPath,
|
||||
cfg.MustReload, // OnChange
|
||||
func() { os.Exit(1) }, // OnDelete
|
||||
)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (cfg *config) Load() error {
|
||||
cfg.mutex.Lock()
|
||||
defer cfg.mutex.Unlock()
|
||||
|
||||
// unload if any
|
||||
cfg.StopProviders()
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read config file: %v", err)
|
||||
}
|
||||
|
||||
cfg.Providers = make(map[string]*Provider)
|
||||
if err = yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return fmt.Errorf("unable to parse config file: %v", err)
|
||||
}
|
||||
|
||||
for name, p := range cfg.Providers {
|
||||
err := p.Init(name)
|
||||
if err != nil {
|
||||
cfgl.Errorf("failed to initialize provider %q %v", name, err)
|
||||
cfg.Providers[name] = nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *config) MustLoad() {
|
||||
if err := cfg.Load(); err != nil {
|
||||
cfgl.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *config) GetAutoCertProvider() (AutoCertProvider, error) {
|
||||
return cfg.AutoCert.GetProvider()
|
||||
}
|
||||
|
||||
func (cfg *config) Reload() error {
|
||||
return cfg.Load()
|
||||
}
|
||||
|
||||
func (cfg *config) MustReload() {
|
||||
cfg.MustLoad()
|
||||
}
|
||||
|
||||
func (cfg *config) StartProviders() {
|
||||
if cfg.Providers == nil {
|
||||
cfgl.Fatal("providers not loaded")
|
||||
}
|
||||
// Providers have their own mutex, no lock needed
|
||||
ParallelForEachValue(cfg.Providers, (*Provider).StartAllRoutes)
|
||||
}
|
||||
|
||||
func (cfg *config) StopProviders() {
|
||||
if cfg.Providers != nil {
|
||||
// Providers have their own mutex, no lock needed
|
||||
ParallelForEachValue(cfg.Providers, (*Provider).StopAllRoutes)
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *config) WatchChanges() {
|
||||
cfg.watcher.Start()
|
||||
}
|
||||
|
||||
func (cfg *config) StopWatching() {
|
||||
cfg.watcher.Stop()
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Providers map[string]*Provider `yaml:",flow"`
|
||||
AutoCert AutoCertConfig `yaml:",flow"`
|
||||
watcher Watcher
|
||||
mutex sync.Mutex
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
ImageNamePortMap = map[string]string{
|
||||
"postgres": "5432",
|
||||
"mysql": "3306",
|
||||
"mariadb": "3306",
|
||||
"redis": "6379",
|
||||
"mssql": "1433",
|
||||
"memcached": "11211",
|
||||
"rabbitmq": "5672",
|
||||
"mongo": "27017",
|
||||
}
|
||||
ExtraNamePortMap = map[string]string{
|
||||
"dns": "53",
|
||||
"ssh": "22",
|
||||
"ftp": "21",
|
||||
"smtp": "25",
|
||||
"pop3": "110",
|
||||
"imap": "143",
|
||||
}
|
||||
NamePortMap = func() map[string]string {
|
||||
m := make(map[string]string)
|
||||
for k, v := range ImageNamePortMap {
|
||||
m[k] = v
|
||||
}
|
||||
for k, v := range ExtraNamePortMap {
|
||||
m[k] = v
|
||||
}
|
||||
return m
|
||||
}()
|
||||
)
|
||||
|
||||
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
|
||||
}()
|
||||
)
|
||||
|
||||
const wildcardLabelPrefix = "proxy.*."
|
||||
|
||||
const clientUrlFromEnv = "FROM_ENV"
|
||||
|
||||
const (
|
||||
certFileDefault = "certs/cert.crt"
|
||||
keyFileDefault = "certs/priv.key"
|
||||
configPath = "config.yml"
|
||||
templatePath = "templates/panel.html"
|
||||
)
|
||||
|
||||
const StreamStopListenTimeout = 1 * time.Second
|
||||
|
||||
const udpBufferSize = 1500
|
||||
|
||||
var logLevel = func() logrus.Level {
|
||||
switch os.Getenv("GOPROXY_DEBUG") {
|
||||
case "1", "true":
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
return logrus.GetLevel()
|
||||
}()
|
||||
|
||||
var redirectHTTP = os.Getenv("GOPROXY_REDIRECT_HTTP") != "0" && os.Getenv("GOPROXY_REDIRECT_HTTP") != "false"
|
||||
@@ -1,202 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"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 (p *Provider) setConfigField(c *ProxyConfig, label string, value string, prefix string) error {
|
||||
if strings.HasPrefix(label, prefix) {
|
||||
field := strings.TrimPrefix(label, prefix)
|
||||
SetFieldFromSnake(c, field, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) getContainerProxyConfigs(container types.Container, clientIP string) []*ProxyConfig {
|
||||
var aliases []string
|
||||
|
||||
cfgs := make([]*ProxyConfig, 0)
|
||||
|
||||
container_name := strings.TrimPrefix(container.Names[0], "/")
|
||||
aliases_label, ok := container.Labels["proxy.aliases"]
|
||||
|
||||
if !ok {
|
||||
aliases = []string{container_name}
|
||||
} else {
|
||||
aliases = strings.Split(aliases_label, ",")
|
||||
}
|
||||
|
||||
isRemote := clientIP != ""
|
||||
|
||||
for _, alias := range aliases {
|
||||
l := p.l.WithField("container", container_name).WithField("alias", alias)
|
||||
config := NewProxyConfig(p)
|
||||
prefix := fmt.Sprintf("proxy.%s.", alias)
|
||||
for label, value := range container.Labels {
|
||||
err := p.setConfigField(&config, label, value, prefix)
|
||||
if err != nil {
|
||||
l.Error(err)
|
||||
}
|
||||
err = p.setConfigField(&config, label, value, wildcardLabelPrefix)
|
||||
if err != nil {
|
||||
l.Error(err)
|
||||
}
|
||||
}
|
||||
if config.Port == "" {
|
||||
config.Port = fmt.Sprintf("%d", selectPort(container))
|
||||
}
|
||||
if config.Port == "0" {
|
||||
// no ports exposed or specified
|
||||
l.Debugf("no ports exposed, ignored")
|
||||
continue
|
||||
}
|
||||
if config.Scheme == "" {
|
||||
switch {
|
||||
case strings.HasSuffix(config.Port, "443"):
|
||||
config.Scheme = "https"
|
||||
case strings.HasPrefix(container.Image, "sha256:"):
|
||||
config.Scheme = "http"
|
||||
default:
|
||||
imageSplit := strings.Split(container.Image, "/")
|
||||
imageSplit = strings.Split(imageSplit[len(imageSplit)-1], ":")
|
||||
imageName := imageSplit[0]
|
||||
_, isKnownImage := ImageNamePortMap[imageName]
|
||||
if isKnownImage {
|
||||
config.Scheme = "tcp"
|
||||
} else {
|
||||
config.Scheme = "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isValidScheme(config.Scheme) {
|
||||
l.Warnf("unsupported scheme: %s, using http", config.Scheme)
|
||||
config.Scheme = "http"
|
||||
}
|
||||
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 = container_name
|
||||
}
|
||||
config.Alias = alias
|
||||
|
||||
cfgs = append(cfgs, &config)
|
||||
}
|
||||
return cfgs
|
||||
}
|
||||
|
||||
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() ([]*ProxyConfig, error) {
|
||||
var clientIP string
|
||||
|
||||
if p.Value == clientUrlFromEnv {
|
||||
clientIP = ""
|
||||
} else {
|
||||
url, err := client.ParseHostURL(p.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse docker host url: %v", err)
|
||||
}
|
||||
clientIP = strings.Split(url.Host, ":")[0]
|
||||
}
|
||||
|
||||
dockerClient, err := p.getDockerClient()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create docker client: %v", err)
|
||||
}
|
||||
|
||||
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
containerSlice, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to list containers: %v", err)
|
||||
}
|
||||
|
||||
cfgs := make([]*ProxyConfig, 0)
|
||||
|
||||
for _, container := range containerSlice {
|
||||
cfgs = append(cfgs, p.getContainerProxyConfigs(container, clientIP)...)
|
||||
}
|
||||
|
||||
return cfgs, nil
|
||||
}
|
||||
|
||||
// var dockerUrlRegex = regexp.MustCompile(`^(?P<scheme>\w+)://(?P<host>[^:]+)(?P<port>:\d+)?(?P<path>/.*)?$`)
|
||||
|
||||
func getPublicPort(p types.Port) uint16 { return p.PublicPort }
|
||||
func getPrivatePort(p types.Port) uint16 { return p.PrivatePort }
|
||||
|
||||
func selectPort(c types.Container) uint16 {
|
||||
if c.HostConfig.NetworkMode == "host" {
|
||||
return selectPortInternal(c, getPrivatePort)
|
||||
}
|
||||
return selectPortInternal(c, getPublicPort)
|
||||
}
|
||||
|
||||
func selectPortInternal(c types.Container, getPort func(types.Port) uint16) uint16 {
|
||||
for _, p := range c.Ports {
|
||||
if port := getPort(p); port != 0 {
|
||||
return port
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func (p *Provider) getFileProxyConfigs() ([]*ProxyConfig, error) {
|
||||
path := p.Value
|
||||
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read config file %q: %v", path, err)
|
||||
}
|
||||
configMap := make(map[string]ProxyConfig, 0)
|
||||
configs := make([]*ProxyConfig, 0)
|
||||
err = yaml.Unmarshal(data, &configMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse config file %q: %v", path, err)
|
||||
}
|
||||
|
||||
for alias, cfg := range configMap {
|
||||
cfg.Alias = alias
|
||||
err = cfg.SetDefaults()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configs = append(configs, &cfg)
|
||||
}
|
||||
return configs, nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("file not found: %s", path)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -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,177 +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) {
|
||||
url, err := url.Parse(fmt.Sprintf("%s://%s:%s", config.Scheme, config.Host, config.Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tr *http.Transport
|
||||
if config.NoTLSVerify {
|
||||
tr = transportNoTLS
|
||||
} else {
|
||||
tr = transport
|
||||
}
|
||||
|
||||
proxy := NewSingleHostReverseProxy(url, tr)
|
||||
|
||||
if !isValidProxyPathMode(config.PathMode) {
|
||||
return nil, fmt.Errorf("invalid path mode: %s", config.PathMode)
|
||||
}
|
||||
|
||||
route := &HTTPRoute{
|
||||
Alias: config.Alias,
|
||||
Url: url,
|
||||
Path: config.Path,
|
||||
Proxy: proxy,
|
||||
PathMode: config.PathMode,
|
||||
l: hrlog.WithFields(logrus.Fields{
|
||||
"alias": config.Alias,
|
||||
"path": config.Path,
|
||||
"path_mode": config.PathMode,
|
||||
}),
|
||||
}
|
||||
|
||||
var rewriteBegin = proxy.Rewrite
|
||||
var rewrite func(*ProxyRequest)
|
||||
var modifyResponse func(*http.Response) error
|
||||
|
||||
switch {
|
||||
case config.Path == "", config.PathMode == ProxyPathMode_Forward:
|
||||
rewrite = rewriteBegin
|
||||
case config.PathMode == ProxyPathMode_Sub:
|
||||
rewrite = func(pr *ProxyRequest) {
|
||||
rewriteBegin(pr)
|
||||
// disable compression
|
||||
pr.Out.Header.Set("Accept-Encoding", "identity")
|
||||
// remove path prefix
|
||||
pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path)
|
||||
}
|
||||
modifyResponse = func(r *http.Response) error {
|
||||
contentType, ok := r.Header["Content-Type"]
|
||||
if !ok || len(contentType) == 0 {
|
||||
route.l.Debug("unknown content type for ", r.Request.URL.String())
|
||||
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)
|
||||
default:
|
||||
route.l.Debug("unknown content type(s): ", contentType)
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to remove path prefix %s: %v", config.Path, err)
|
||||
route.l.WithField("action", "path_sub").Error(err)
|
||||
r.Status = err.Error()
|
||||
r.StatusCode = http.StatusInternalServerError
|
||||
}
|
||||
return err
|
||||
}
|
||||
default:
|
||||
rewrite = func(pr *ProxyRequest) {
|
||||
rewriteBegin(pr)
|
||||
pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path)
|
||||
}
|
||||
}
|
||||
|
||||
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() {}
|
||||
func (r *HTTPRoute) Stop() {
|
||||
httpRoutes.Delete(r.Alias)
|
||||
}
|
||||
|
||||
func isValidProxyPathMode(mode string) bool {
|
||||
switch mode {
|
||||
case ProxyPathMode_Forward, ProxyPathMode_Sub, ProxyPathMode_RemovedPath:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
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, fmt.Errorf("no matching route for subdomain %s", subdomain)
|
||||
}
|
||||
|
||||
func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
route, err := findHTTPRoute(r.Host, r.URL.Path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("request failed %s %s%s, error: %v",
|
||||
r.Method,
|
||||
r.Host,
|
||||
r.URL.Path,
|
||||
err,
|
||||
)
|
||||
logrus.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
route.Proxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// alias -> (path -> routes)
|
||||
type HTTPRoutes = SafeMap[string, *pathPoolMap]
|
||||
|
||||
var httpRoutes HTTPRoutes = NewSafeMap[string](newPathPoolMap)
|
||||
@@ -1,11 +0,0 @@
|
||||
package main
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
var palog = logrus.WithField("component", "panel")
|
||||
var prlog = logrus.WithField("component", "provider")
|
||||
var cfgl = logrus.WithField("component", "config")
|
||||
var hrlog = logrus.WithField("component", "http_proxy")
|
||||
var srlog = logrus.WithField("component", "stream")
|
||||
var wlog = logrus.WithField("component", "watcher")
|
||||
var aclog = logrus.WithField("component", "autocert")
|
||||
@@ -1,96 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
ForceColors: true,
|
||||
DisableColors: false,
|
||||
FullTimestamp: true,
|
||||
})
|
||||
|
||||
cfg := NewConfig()
|
||||
cfg.MustLoad()
|
||||
|
||||
autoCertProvider, err := cfg.GetAutoCertProvider()
|
||||
|
||||
if err != nil {
|
||||
aclog.Warn(err)
|
||||
autoCertProvider = nil
|
||||
}
|
||||
|
||||
var httpProxyHandler http.Handler
|
||||
var httpPanelHandler http.Handler
|
||||
|
||||
var proxyServer *Server
|
||||
var panelServer *Server
|
||||
|
||||
if redirectHTTP {
|
||||
httpProxyHandler = http.HandlerFunc(redirectToTLSHandler)
|
||||
httpPanelHandler = http.HandlerFunc(redirectToTLSHandler)
|
||||
} else {
|
||||
httpProxyHandler = http.HandlerFunc(proxyHandler)
|
||||
httpPanelHandler = http.HandlerFunc(panelHandler)
|
||||
}
|
||||
|
||||
if autoCertProvider != nil {
|
||||
ok := autoCertProvider.LoadCert()
|
||||
if !ok {
|
||||
err := autoCertProvider.ObtainCert()
|
||||
if err != nil {
|
||||
aclog.Fatal("error obtaining certificate ", err)
|
||||
}
|
||||
}
|
||||
for name, expiry := range autoCertProvider.GetExpiries() {
|
||||
aclog.Infof("certificate %q: expire on %v", name, expiry)
|
||||
}
|
||||
go autoCertProvider.ScheduleRenewal()
|
||||
}
|
||||
proxyServer = NewServer(
|
||||
"proxy",
|
||||
autoCertProvider,
|
||||
":80",
|
||||
httpProxyHandler,
|
||||
":443",
|
||||
http.HandlerFunc(proxyHandler),
|
||||
)
|
||||
panelServer = NewServer(
|
||||
"panel",
|
||||
autoCertProvider,
|
||||
":8080",
|
||||
httpPanelHandler,
|
||||
":8443",
|
||||
http.HandlerFunc(panelHandler),
|
||||
)
|
||||
|
||||
proxyServer.Start()
|
||||
panelServer.Start()
|
||||
|
||||
InitFSWatcher()
|
||||
InitDockerWatcher()
|
||||
|
||||
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
|
||||
// cfg.StopWatching()
|
||||
|
||||
cfg.StopProviders()
|
||||
panelServer.Stop()
|
||||
proxyServer.Stop()
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package main
|
||||
|
||||
import "sync"
|
||||
|
||||
type safeMap[KT comparable, VT interface{}] struct {
|
||||
SafeMap[KT, VT]
|
||||
m map[KT]VT
|
||||
mutex sync.Mutex
|
||||
defaultFactory func() VT
|
||||
}
|
||||
|
||||
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 NewSafeMap[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.mutex.Lock()
|
||||
m.m[key] = value
|
||||
m.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *safeMap[KT, VT]) Ensure(key KT) {
|
||||
m.mutex.Lock()
|
||||
if _, ok := m.m[key]; !ok {
|
||||
m.m[key] = m.defaultFactory()
|
||||
}
|
||||
m.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *safeMap[KT, VT]) Get(key KT) VT {
|
||||
m.mutex.Lock()
|
||||
value := m.m[key]
|
||||
m.mutex.Unlock()
|
||||
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.mutex.Lock()
|
||||
delete(m.m, key)
|
||||
m.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *safeMap[KT, VT]) Clear() {
|
||||
m.mutex.Lock()
|
||||
m.m = make(map[KT]VT)
|
||||
m.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *safeMap[KT, VT]) Size() int {
|
||||
m.mutex.Lock()
|
||||
size := len(m.m)
|
||||
m.mutex.Unlock()
|
||||
return size
|
||||
}
|
||||
|
||||
func (m *safeMap[KT, VT]) Contains(key KT) bool {
|
||||
m.mutex.Lock()
|
||||
_, ok := m.m[key]
|
||||
m.mutex.Unlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *safeMap[KT, VT]) ForEach(fn func(key KT, value VT)) {
|
||||
m.mutex.Lock()
|
||||
for k, v := range m.m {
|
||||
fn(k, v)
|
||||
}
|
||||
m.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *safeMap[KT, VT]) Iterator() map[KT]VT {
|
||||
return m.m
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
var 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,
|
||||
},
|
||||
}
|
||||
|
||||
func panelHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/":
|
||||
panelIndex(w, r)
|
||||
return
|
||||
case "/checkhealth":
|
||||
panelCheckTargetHealth(w, r)
|
||||
return
|
||||
default:
|
||||
palog.Errorf("%s not found", r.URL.Path)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func panelIndex(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(templatePath)
|
||||
|
||||
if err != nil {
|
||||
palog.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
type allRoutes struct {
|
||||
HTTPRoutes HTTPRoutes
|
||||
StreamRoutes StreamRoutes
|
||||
}
|
||||
|
||||
err = tmpl.Execute(w, allRoutes{
|
||||
HTTPRoutes: httpRoutes,
|
||||
StreamRoutes: streamRoutes,
|
||||
})
|
||||
if err != nil {
|
||||
palog.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func panelCheckTargetHealth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodHead {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
targetUrl := r.URL.Query().Get("target")
|
||||
|
||||
if targetUrl == "" {
|
||||
http.Error(w, "target is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
url, err := url.Parse(targetUrl)
|
||||
if err != nil {
|
||||
palog.Infof("failed to parse url %q, error: %v", targetUrl, err)
|
||||
http.Error(w, err.Error(), 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)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type pathPoolMap struct {
|
||||
SafeMap[string, *httpLoadBalancePool]
|
||||
}
|
||||
|
||||
func newPathPoolMap() *pathPoolMap {
|
||||
return &pathPoolMap{
|
||||
NewSafeMap[string](NewHTTPLoadBalancePool),
|
||||
}
|
||||
}
|
||||
|
||||
func (m pathPoolMap) Add(path string, route *HTTPRoute) {
|
||||
m.Ensure(path)
|
||||
m.Get(path).Add(route)
|
||||
}
|
||||
|
||||
func (m pathPoolMap) FindMatch(pathGot string) (*HTTPRoute, error) {
|
||||
for pathWant, v := range m.Iterator() {
|
||||
if strings.HasPrefix(pathGot, pathWant) {
|
||||
return v.Pick(), nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no matching route for path %s", pathGot)
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
Kind string // docker, file
|
||||
Value string
|
||||
|
||||
watcher Watcher
|
||||
routes map[string]Route // id -> Route
|
||||
mutex sync.Mutex
|
||||
l logrus.FieldLogger
|
||||
}
|
||||
|
||||
// Init is called after LoadProxyConfig
|
||||
func (p *Provider) Init(name string) error {
|
||||
p.l = prlog.WithFields(logrus.Fields{"kind": p.Kind, "name": name})
|
||||
|
||||
if err := p.loadProxyConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.initWatcher()
|
||||
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 = make(map[string]Route)
|
||||
}
|
||||
|
||||
func (p *Provider) ReloadRoutes() {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
p.StopAllRoutes()
|
||||
err := p.loadProxyConfig()
|
||||
if err != nil {
|
||||
p.l.Error("failed to reload routes: ", err)
|
||||
return
|
||||
}
|
||||
p.StartAllRoutes()
|
||||
}
|
||||
|
||||
func (p *Provider) loadProxyConfig() error {
|
||||
var cfgs []*ProxyConfig
|
||||
var err error
|
||||
|
||||
switch p.Kind {
|
||||
case ProviderKind_Docker:
|
||||
cfgs, err = p.getDockerProxyConfigs()
|
||||
case ProviderKind_File:
|
||||
cfgs, err = p.getFileProxyConfigs()
|
||||
default:
|
||||
// this line should never be reached
|
||||
return fmt.Errorf("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))
|
||||
for _, cfg := range cfgs {
|
||||
r, err := NewRoute(cfg)
|
||||
if err != nil {
|
||||
p.l.Errorf("error creating route %s: %v", cfg.Alias, err)
|
||||
continue
|
||||
}
|
||||
p.routes[cfg.GetID()] = r
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) initWatcher() error {
|
||||
switch p.Kind {
|
||||
case ProviderKind_Docker:
|
||||
var err error
|
||||
dockerClient, err := p.getDockerClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create docker client: %v", err)
|
||||
}
|
||||
p.watcher = NewDockerWatcher(dockerClient, p.ReloadRoutes)
|
||||
case ProviderKind_File:
|
||||
p.watcher = NewFileWatcher(p.Value, p.ReloadRoutes, p.StopAllRoutes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ProxyConfig struct {
|
||||
Alias string
|
||||
Scheme string
|
||||
Host string
|
||||
Port string
|
||||
LoadBalance string // docker provider only
|
||||
NoTLSVerify bool // http proxy only
|
||||
Path string // http proxy only
|
||||
PathMode string `yaml:"path_mode"` // http proxy only
|
||||
|
||||
provider *Provider
|
||||
}
|
||||
|
||||
func NewProxyConfig(provider *Provider) ProxyConfig {
|
||||
return ProxyConfig{
|
||||
provider: provider,
|
||||
}
|
||||
}
|
||||
|
||||
// used by `GetFileProxyConfigs`
|
||||
func (cfg *ProxyConfig) SetDefaults() error {
|
||||
if cfg.Alias == "" {
|
||||
return fmt.Errorf("alias is required")
|
||||
}
|
||||
if cfg.Scheme == "" {
|
||||
cfg.Scheme = "http"
|
||||
}
|
||||
if cfg.Host == "" {
|
||||
return fmt.Errorf("host is required for %q", cfg.Alias)
|
||||
}
|
||||
if cfg.Port == "" {
|
||||
cfg.Port = "80"
|
||||
}
|
||||
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,56 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Route interface {
|
||||
Start()
|
||||
Stop()
|
||||
}
|
||||
|
||||
func NewRoute(cfg *ProxyConfig) (Route, error) {
|
||||
if isStreamScheme(cfg.Scheme) {
|
||||
id := cfg.GetID()
|
||||
if streamRoutes.Contains(id) {
|
||||
return nil, fmt.Errorf("duplicated %s stream %s, ignoring", cfg.Scheme, id)
|
||||
}
|
||||
route, err := NewStreamRoute(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
streamRoutes.Set(id, route)
|
||||
return route, nil
|
||||
} else {
|
||||
httpRoutes.Ensure(cfg.Alias)
|
||||
route, err := NewHTTPRoute(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRoutes.Get(cfg.Alias).Add(cfg.Path, route)
|
||||
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
|
||||
}
|
||||
|
||||
// id -> target
|
||||
type StreamRoutes = SafeMap[string, StreamRoute]
|
||||
|
||||
var streamRoutes = NewSafeMap[string, StreamRoute]()
|
||||
@@ -1,99 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"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
|
||||
}
|
||||
|
||||
func NewServer(name string, provider AutoCertProvider, httpAddr string, httpHandler http.Handler, httpsAddr string, httpsHandler http.Handler) *Server {
|
||||
if provider != nil {
|
||||
return &Server{
|
||||
Name: name,
|
||||
CertProvider: provider,
|
||||
http: &http.Server{
|
||||
Addr: httpAddr,
|
||||
Handler: httpHandler,
|
||||
},
|
||||
https: &http.Server{
|
||||
Addr: httpsAddr,
|
||||
Handler: httpsHandler,
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: provider.GetCert,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return &Server{
|
||||
Name: name,
|
||||
KeyFile: keyFileDefault,
|
||||
CertFile: certFileDefault,
|
||||
http: &http.Server{
|
||||
Addr: httpAddr,
|
||||
Handler: httpHandler,
|
||||
},
|
||||
https: &http.Server{
|
||||
Addr: httpsAddr,
|
||||
Handler: httpsHandler,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 || utils.fileOK(s.CertFile) && utils.fileOK(s.KeyFile)) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type StreamRoute interface {
|
||||
Route
|
||||
ListeningUrl() string
|
||||
TargetUrl() string
|
||||
Logger() logrus.FieldLogger
|
||||
|
||||
closeListeners()
|
||||
closeChannel()
|
||||
unmarkPort()
|
||||
wait()
|
||||
}
|
||||
|
||||
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
|
||||
stopChann chan struct{}
|
||||
l logrus.FieldLogger
|
||||
}
|
||||
|
||||
func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
|
||||
var streamType string = StreamType_TCP
|
||||
var srcPort string
|
||||
var dstPort string
|
||||
var srcScheme string
|
||||
var dstScheme string
|
||||
|
||||
port_split := strings.Split(config.Port, ":")
|
||||
if len(port_split) != 2 {
|
||||
cfgl.Warnf("invalid port %s, assuming it is target port", config.Port)
|
||||
srcPort = "0"
|
||||
dstPort = config.Port
|
||||
} else {
|
||||
srcPort = port_split[0]
|
||||
dstPort = port_split[1]
|
||||
}
|
||||
|
||||
if port, hasName := NamePortMap[dstPort]; hasName {
|
||||
dstPort = port
|
||||
}
|
||||
|
||||
srcPortInt, err := strconv.Atoi(srcPort)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid stream source port %s, ignoring", srcPort,
|
||||
)
|
||||
}
|
||||
|
||||
utils.markPortInUse(srcPortInt)
|
||||
|
||||
dstPortInt, err := strconv.Atoi(dstPort)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid stream target port %s, ignoring", dstPort,
|
||||
)
|
||||
}
|
||||
|
||||
scheme_split := strings.Split(config.Scheme, ":")
|
||||
|
||||
if len(scheme_split) == 2 {
|
||||
srcScheme = scheme_split[0]
|
||||
dstScheme = scheme_split[1]
|
||||
} else {
|
||||
srcScheme = config.Scheme
|
||||
dstScheme = config.Scheme
|
||||
}
|
||||
|
||||
return &StreamRouteBase{
|
||||
Alias: config.Alias,
|
||||
Type: streamType,
|
||||
ListeningScheme: srcScheme,
|
||||
ListeningPort: srcPortInt,
|
||||
TargetScheme: dstScheme,
|
||||
TargetHost: config.Host,
|
||||
TargetPort: dstPortInt,
|
||||
|
||||
id: config.GetID(),
|
||||
wg: sync.WaitGroup{},
|
||||
stopChann: make(chan struct{}, 1),
|
||||
l: srlog.WithFields(logrus.Fields{
|
||||
"alias": config.Alias,
|
||||
"src": fmt.Sprintf("%s://:%d", srcScheme, srcPortInt),
|
||||
"dst": fmt.Sprintf("%s://%s:%d", dstScheme, config.Host, dstPortInt),
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewStreamRoute(config *ProxyConfig) (StreamRoute, error) {
|
||||
switch config.Scheme {
|
||||
case StreamType_TCP:
|
||||
return NewTCPRoute(config)
|
||||
case StreamType_UDP:
|
||||
return NewUDPRoute(config)
|
||||
default:
|
||||
return nil, errors.New("unknown stream type")
|
||||
}
|
||||
}
|
||||
|
||||
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) setupListen() {
|
||||
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) wait() {
|
||||
route.wg.Wait()
|
||||
}
|
||||
|
||||
func (route *StreamRouteBase) closeChannel() {
|
||||
close(route.stopChann)
|
||||
}
|
||||
|
||||
func (route *StreamRouteBase) unmarkPort() {
|
||||
utils.unmarkPortInUse(route.ListeningPort)
|
||||
}
|
||||
|
||||
func stopListening(route StreamRoute) {
|
||||
l := route.Logger()
|
||||
l.Debug("stopping listening")
|
||||
|
||||
// close channel -> wait -> close listeners
|
||||
|
||||
route.closeChannel()
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
route.wait()
|
||||
close(done)
|
||||
route.unmarkPort()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
l.Info("stopped listening")
|
||||
case <-time.After(StreamStopListenTimeout):
|
||||
l.Error("timed out waiting for connections")
|
||||
}
|
||||
|
||||
route.closeListeners()
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const tcpDialTimeout = 5 * time.Second
|
||||
|
||||
type TCPRoute struct {
|
||||
*StreamRouteBase
|
||||
listener net.Listener
|
||||
connChan chan net.Conn
|
||||
}
|
||||
|
||||
func NewTCPRoute(config *ProxyConfig) (StreamRoute, error) {
|
||||
base, err := newStreamRouteBase(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if base.TargetScheme != StreamType_TCP {
|
||||
return nil, fmt.Errorf("tcp to %s not yet supported", base.TargetScheme)
|
||||
}
|
||||
return &TCPRoute{
|
||||
StreamRouteBase: base,
|
||||
listener: nil,
|
||||
connChan: make(chan net.Conn),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (route *TCPRoute) Start() {
|
||||
route.setupListen()
|
||||
in, err := net.Listen("tcp", fmt.Sprintf(":%v", route.ListeningPort))
|
||||
if err != nil {
|
||||
route.l.Error(err)
|
||||
return
|
||||
}
|
||||
route.listener = in
|
||||
route.wg.Add(2)
|
||||
go route.grAcceptConnections()
|
||||
go route.grHandleConnections()
|
||||
}
|
||||
|
||||
func (route *TCPRoute) Stop() {
|
||||
stopListening(route)
|
||||
streamRoutes.Delete(route.id)
|
||||
}
|
||||
|
||||
func (route *TCPRoute) closeListeners() {
|
||||
if route.listener == nil {
|
||||
return
|
||||
}
|
||||
route.listener.Close()
|
||||
route.listener = nil
|
||||
}
|
||||
|
||||
func (route *TCPRoute) grAcceptConnections() {
|
||||
defer route.wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-route.stopChann:
|
||||
return
|
||||
default:
|
||||
conn, err := route.listener.Accept()
|
||||
if err != nil {
|
||||
route.l.Error(err)
|
||||
continue
|
||||
}
|
||||
route.connChan <- conn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (route *TCPRoute) grHandleConnections() {
|
||||
defer route.wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-route.stopChann:
|
||||
return
|
||||
case conn := <-route.connChan:
|
||||
route.wg.Add(1)
|
||||
go route.grHandleConnection(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (route *TCPRoute) grHandleConnection(clientConn net.Conn) {
|
||||
defer clientConn.Close()
|
||||
defer route.wg.Done()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tcpDialTimeout)
|
||||
defer cancel()
|
||||
|
||||
serverAddr := fmt.Sprintf("%s:%v", route.TargetHost, route.TargetPort)
|
||||
dialer := &net.Dialer{}
|
||||
serverConn, err := dialer.DialContext(ctx, route.TargetScheme, serverAddr)
|
||||
if err != nil {
|
||||
route.l.WithField("stage", "dial").Infof("%v", err)
|
||||
return
|
||||
}
|
||||
route.tcpPipe(clientConn, serverConn)
|
||||
}
|
||||
|
||||
func (route *TCPRoute) tcpPipe(src net.Conn, dest net.Conn) {
|
||||
close := func() {
|
||||
src.Close()
|
||||
dest.Close()
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2) // Number of goroutines
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(src, dest)
|
||||
route.l.Error(err)
|
||||
close()
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(dest, src)
|
||||
route.l.Error(err)
|
||||
close()
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type UDPRoute struct {
|
||||
*StreamRouteBase
|
||||
|
||||
connMap map[net.Addr]net.Conn
|
||||
connMapMutex sync.Mutex
|
||||
|
||||
listeningConn *net.UDPConn
|
||||
targetConn *net.UDPConn
|
||||
|
||||
connChan chan *UDPConn
|
||||
}
|
||||
|
||||
type UDPConn struct {
|
||||
remoteAddr net.Addr
|
||||
buffer []byte
|
||||
bytesReceived []byte
|
||||
nReceived int
|
||||
}
|
||||
|
||||
func NewUDPRoute(config *ProxyConfig) (StreamRoute, error) {
|
||||
base, err := newStreamRouteBase(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if base.TargetScheme != StreamType_UDP {
|
||||
return nil, fmt.Errorf("udp to %s not yet supported", base.TargetScheme)
|
||||
}
|
||||
|
||||
return &UDPRoute{
|
||||
StreamRouteBase: base,
|
||||
connMap: make(map[net.Addr]net.Conn),
|
||||
connChan: make(chan *UDPConn),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (route *UDPRoute) Start() {
|
||||
route.setupListen()
|
||||
|
||||
source, err := net.ListenPacket(route.ListeningScheme, fmt.Sprintf(":%v", route.ListeningPort))
|
||||
if err != nil {
|
||||
route.l.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
target, err := net.Dial(route.TargetScheme, fmt.Sprintf("%s:%v", route.TargetHost, route.TargetPort))
|
||||
if err != nil {
|
||||
route.l.Error(err)
|
||||
source.Close()
|
||||
return
|
||||
}
|
||||
|
||||
route.listeningConn = source.(*net.UDPConn)
|
||||
route.targetConn = target.(*net.UDPConn)
|
||||
|
||||
route.wg.Add(2)
|
||||
go route.grAcceptConnections()
|
||||
go route.grHandleConnections()
|
||||
}
|
||||
|
||||
func (route *UDPRoute) Stop() {
|
||||
stopListening(route)
|
||||
streamRoutes.Delete(route.id)
|
||||
}
|
||||
|
||||
func (route *UDPRoute) closeListeners() {
|
||||
if route.listeningConn != nil {
|
||||
route.listeningConn.Close()
|
||||
route.listeningConn = nil
|
||||
}
|
||||
if route.targetConn != nil {
|
||||
route.targetConn.Close()
|
||||
route.targetConn = nil
|
||||
}
|
||||
for _, conn := range route.connMap {
|
||||
conn.(*net.UDPConn).Close() // TODO: change on non udp target
|
||||
}
|
||||
route.connMap = make(map[net.Addr]net.Conn)
|
||||
}
|
||||
|
||||
func (route *UDPRoute) grAcceptConnections() {
|
||||
defer route.wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-route.stopChann:
|
||||
return
|
||||
default:
|
||||
conn, err := route.accept()
|
||||
if err != nil {
|
||||
route.l.Error(err)
|
||||
continue
|
||||
}
|
||||
route.connChan <- conn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (route *UDPRoute) grHandleConnections() {
|
||||
defer route.wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-route.stopChann:
|
||||
return
|
||||
case conn := <-route.connChan:
|
||||
go func() {
|
||||
err := route.handleConnection(conn)
|
||||
if err != nil {
|
||||
route.l.Error(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (route *UDPRoute) handleConnection(conn *UDPConn) error {
|
||||
var err error
|
||||
|
||||
srcConn, ok := route.connMap[conn.remoteAddr]
|
||||
if !ok {
|
||||
route.connMapMutex.Lock()
|
||||
srcConn, err = net.DialUDP("udp", nil, conn.remoteAddr.(*net.UDPAddr))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
route.connMap[conn.remoteAddr] = srcConn
|
||||
route.connMapMutex.Unlock()
|
||||
}
|
||||
|
||||
var forwarder func(*UDPConn, net.Conn) error
|
||||
|
||||
if logLevel == logrus.DebugLevel {
|
||||
forwarder = route.forwardReceivedDebug
|
||||
} else {
|
||||
forwarder = route.forwardReceivedReal
|
||||
}
|
||||
|
||||
// initiate connection to target
|
||||
err = forwarder(conn, route.targetConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-route.stopChann:
|
||||
return nil
|
||||
default:
|
||||
// receive from target
|
||||
conn, err = route.readFrom(route.targetConn, conn.buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// forward to source
|
||||
err = forwarder(conn, srcConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// read from source
|
||||
conn, err = route.readFrom(srcConn, conn.buffer)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// forward to target
|
||||
err = forwarder(conn, route.targetConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (route *UDPRoute) accept() (*UDPConn, error) {
|
||||
in := route.listeningConn
|
||||
|
||||
buffer := make([]byte, udpBufferSize)
|
||||
nRead, srcAddr, err := in.ReadFromUDP(buffer)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if nRead == 0 {
|
||||
return nil, io.ErrShortBuffer
|
||||
}
|
||||
|
||||
return &UDPConn{
|
||||
remoteAddr: srcAddr,
|
||||
buffer: buffer,
|
||||
bytesReceived: buffer[:nRead],
|
||||
nReceived: nRead},
|
||||
nil
|
||||
}
|
||||
|
||||
func (route *UDPRoute) readFrom(src net.Conn, buffer []byte) (*UDPConn, error) {
|
||||
nRead, err := src.Read(buffer)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if nRead == 0 {
|
||||
return nil, io.ErrShortBuffer
|
||||
}
|
||||
|
||||
return &UDPConn{
|
||||
remoteAddr: src.RemoteAddr(),
|
||||
buffer: buffer,
|
||||
bytesReceived: buffer[:nRead],
|
||||
nReceived: nRead,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (route *UDPRoute) forwardReceivedReal(receivedConn *UDPConn, dest net.Conn) error {
|
||||
nWritten, err := dest.Write(receivedConn.bytesReceived)
|
||||
|
||||
if nWritten != receivedConn.nReceived {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (route *UDPRoute) forwardReceivedDebug(receivedConn *UDPConn, dest net.Conn) error {
|
||||
route.l.WithField("size", receivedConn.nReceived).Debugf(
|
||||
"forwarding from %s to %s",
|
||||
receivedConn.remoteAddr.String(),
|
||||
dest.RemoteAddr().String(),
|
||||
)
|
||||
return route.forwardReceivedReal(receivedConn, dest)
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
xhtml "golang.org/x/net/html"
|
||||
)
|
||||
|
||||
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, fmt.Errorf("unable to find free port: %v", 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, 5*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.Close()
|
||||
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 fmt.Errorf("unknown field %s", field)
|
||||
}
|
||||
prop.Set(reflect.ValueOf(value))
|
||||
return nil
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
"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 {
|
||||
name string // for log / error output
|
||||
kind string // for log / error output
|
||||
onChange func()
|
||||
l logrus.FieldLogger
|
||||
}
|
||||
|
||||
type fileWatcher struct {
|
||||
*watcherBase
|
||||
path string
|
||||
onDelete func()
|
||||
}
|
||||
|
||||
type dockerWatcher struct {
|
||||
*watcherBase
|
||||
client *client.Client
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func newWatcher(kind string, name string, onChange func()) *watcherBase {
|
||||
return &watcherBase{
|
||||
kind: kind,
|
||||
name: name,
|
||||
onChange: onChange,
|
||||
l: wlog.WithFields(logrus.Fields{"kind": kind, "name": name}),
|
||||
}
|
||||
}
|
||||
func NewFileWatcher(p string, onChange func(), onDelete func()) Watcher {
|
||||
return &fileWatcher{
|
||||
watcherBase: newWatcher("File", path.Base(p), onChange),
|
||||
path: p,
|
||||
onDelete: onDelete,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDockerWatcher(c *client.Client, onChange func()) Watcher {
|
||||
return &dockerWatcher{
|
||||
watcherBase: newWatcher("Docker", c.DaemonHost(), onChange),
|
||||
client: c,
|
||||
stopCh: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *fileWatcher) Start() {
|
||||
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() {
|
||||
if fsWatcher == nil {
|
||||
return
|
||||
}
|
||||
fileWatchMap.Delete(w.path)
|
||||
err := fsWatcher.Remove(w.path)
|
||||
if err != nil {
|
||||
w.l.WithField("action", "stop").Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *fileWatcher) Dispose() {
|
||||
w.Stop()
|
||||
}
|
||||
|
||||
func (w *dockerWatcher) Start() {
|
||||
dockerWatchMap.Set(w.name, w)
|
||||
w.wg.Add(1)
|
||||
go w.watch()
|
||||
}
|
||||
|
||||
func (w *dockerWatcher) Stop() {
|
||||
if w.stopCh == nil {
|
||||
return
|
||||
}
|
||||
close(w.stopCh)
|
||||
w.wg.Wait()
|
||||
w.stopCh = nil
|
||||
dockerWatchMap.Delete(w.name)
|
||||
}
|
||||
|
||||
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 InitDockerWatcher() {
|
||||
// stop all docker client on watcher stop
|
||||
go func() {
|
||||
<-dockerWatcherStop
|
||||
ParallelForEachValue(
|
||||
dockerWatchMap.Iterator(),
|
||||
(*dockerWatcher).Dispose,
|
||||
)
|
||||
dockerWatcherWg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
func StopFSWatcher() {
|
||||
close(fsWatcherStop)
|
||||
fsWatcherWg.Wait()
|
||||
}
|
||||
|
||||
func StopDockerWatcher() {
|
||||
close(dockerWatcherStop)
|
||||
dockerWatcherWg.Wait()
|
||||
}
|
||||
|
||||
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")
|
||||
go w.onChange()
|
||||
case event.Has(fsnotify.Remove), event.Has(fsnotify.Rename):
|
||||
w.l.Info("file renamed / deleted")
|
||||
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:
|
||||
w.l.Infof("container %s %s", msg.Actor.Attributes["name"], msg.Action)
|
||||
go w.onChange()
|
||||
case err := <-errChan:
|
||||
w.l.Errorf("%s, retrying in 1s", err)
|
||||
time.Sleep(1 * time.Second)
|
||||
msgChan, errChan = listen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fsWatcher *fsnotify.Watcher
|
||||
var (
|
||||
fileWatchMap = NewSafeMap[string, *fileWatcher]()
|
||||
dockerWatchMap = NewSafeMap[string, *dockerWatcher]()
|
||||
)
|
||||
var (
|
||||
fsWatcherStop = make(chan struct{}, 1)
|
||||
dockerWatcherStop = make(chan struct{}, 1)
|
||||
)
|
||||
var (
|
||||
fsWatcherWg sync.WaitGroup
|
||||
dockerWatcherWg sync.WaitGroup
|
||||
)
|
||||
54
src/go.mod
Normal file
54
src/go.mod
Normal file
@@ -0,0 +1,54 @@
|
||||
module github.com/yusing/go-proxy
|
||||
|
||||
go 1.22
|
||||
|
||||
toolchain go1.23.1
|
||||
|
||||
require (
|
||||
github.com/docker/cli v27.2.1+incompatible
|
||||
github.com/docker/docker v27.2.1+incompatible
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/go-acme/lego/v4 v4.18.0
|
||||
github.com/santhosh-tekuri/jsonschema v1.2.4
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
golang.org/x/net v0.29.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
|
||||
go.opentelemetry.io/otel v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.30.0 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/tools v0.25.0 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
145
go.sum → src/go.sum
Executable file → Normal file
145
go.sum → src/go.sum
Executable file → Normal file
@@ -1,51 +1,43 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cloudflare/cloudflare-go v0.86.0 h1:jEKN5VHNYNYtfDL2lUFLTRo+nOVNPFxpXTstVx0rqHI=
|
||||
github.com/cloudflare/cloudflare-go v0.86.0/go.mod h1:wYW/5UP02TUfBToa/yKbQHV+r6h1NnJ1Je7XjuGM4Jw=
|
||||
github.com/cloudflare/cloudflare-go v0.91.0 h1:L7IR+86qrZuEMSjGFg4cwRwtHqC8uCPmMUkP7BD4CPw=
|
||||
github.com/cloudflare/cloudflare-go v0.91.0/go.mod h1:nUqvBUUDRxNzsDSQjbqUNWHEIYAoUlgRmcAzMKlFdKs=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
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/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cloudflare/cloudflare-go v0.104.0 h1:R/lB0dZupaZbOgibAH/BRrkFbZ6Acn/WsKg2iX2xXuY=
|
||||
github.com/cloudflare/cloudflare-go v0.104.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v26.0.0+incompatible h1:90BKrx1a1HKYpSnnBFR6AgDq/FqkHxwlUyzJVPxD30I=
|
||||
github.com/docker/cli v26.0.0+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 v26.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v27.2.1+incompatible h1:U5BPtiD0viUzjGAjV1p0MGB8eVA3L3cbIrnyWmSJI70=
|
||||
github.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI=
|
||||
github.com/docker/docker v27.2.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/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/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
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/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.16.1/go.mod h1:AVvwdPned/IWpD/ihHhMsKnveF7HHYAz/CmtXi7OZoE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
|
||||
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/go-acme/lego/v4 v4.18.0 h1:2hH8KcdRBSb+p5o9VZIm61GAOXYALgILUCSs1Q+OYsk=
|
||||
github.com/go-acme/lego/v4 v4.18.0/go.mod h1:Blkg3izvXpl3zxk7WKngIuwR2I/hvYVP3vRnvgBp7m8=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
||||
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.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
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/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
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/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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -53,26 +45,14 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
|
||||
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/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.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
|
||||
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
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/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
@@ -83,98 +63,89 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
|
||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
|
||||
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI=
|
||||
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
|
||||
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
|
||||
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
|
||||
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
|
||||
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
|
||||
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
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/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
|
||||
google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
|
||||
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ=
|
||||
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-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.63.1 h1:pNClQmvdlyNUiwFETOux/PYqfhmA7BrswEdGRnib1fA=
|
||||
google.golang.org/grpc v1.63.1/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
149
src/main.go
Executable file
149
src/main.go
Executable file
@@ -0,0 +1,149 @@
|
||||
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"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
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("module", "main")
|
||||
|
||||
if common.IsDebug {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableSorting: true,
|
||||
FullTimestamp: true,
|
||||
ForceColors: 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()]()
|
||||
|
||||
// exit if only validate config
|
||||
if args.Command == common.CommandValidate {
|
||||
var err E.NestedError
|
||||
data, err := E.Check(os.ReadFile(common.ConfigPath))
|
||||
if err.IsNotNil() {
|
||||
l.WithError(err).Fatalf("config error")
|
||||
}
|
||||
if err = config.Validate(data); err.IsNotNil() {
|
||||
l.WithError(err).Fatalf("config error")
|
||||
}
|
||||
l.Printf("config OK")
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := config.New()
|
||||
if err.IsNotNil() {
|
||||
l.Fatalf("config error: %s", err)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if autocert != nil {
|
||||
err = autocert.LoadCert()
|
||||
|
||||
if err.IsNotNil() {
|
||||
l.Error(err)
|
||||
l.Info("Now attempting to obtain a new certificate...")
|
||||
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,
|
||||
}
|
||||
}
|
||||
48
src/models/proxy_entry.go
Normal file
48
src/models/proxy_entry.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"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"` // https proxy only
|
||||
PathPatterns []string `yaml:"path_patterns" json:"path_patterns"` // http(s) proxy only
|
||||
SetHeaders map[string]string `yaml:"set_headers" json:"set_headers"` // http(s) proxy only
|
||||
HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http(s) 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.Host == "" {
|
||||
e.Host = "localhost"
|
||||
}
|
||||
switch e.Scheme {
|
||||
case "http":
|
||||
e.Port = "80"
|
||||
case "https":
|
||||
e.Port = "443"
|
||||
}
|
||||
}
|
||||
6
src/models/proxy_providers.go
Normal file
6
src/models/proxy_providers.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type ProxyProviders struct {
|
||||
Files []string `yaml:"include" json:"include"` // docker, file
|
||||
Docker map[string]string `yaml:"docker" json:"docker"`
|
||||
}
|
||||
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...)
|
||||
)
|
||||
|
||||
98
src/proxy/entry.go
Normal file
98
src/proxy/entry.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
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
|
||||
PathPatterns T.PathPatterns
|
||||
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
|
||||
}
|
||||
pathPatterns, err := T.NewPathPatterns(m.PathPatterns)
|
||||
if err.IsNotNil() {
|
||||
return nil, err
|
||||
}
|
||||
setHeaders, err := T.NewHTTPHeaders(m.SetHeaders)
|
||||
if err.IsNotNil() {
|
||||
return nil, err
|
||||
}
|
||||
url, err := E.Check(url.Parse(fmt.Sprintf("%s://%s:%d", s, host, 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,
|
||||
PathPatterns: pathPatterns,
|
||||
SetHeaders: 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 string
|
||||
type Aliases struct{ *F.Slice[Alias] }
|
||||
|
||||
func NewAlias(s string) Alias {
|
||||
return Alias(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
|
||||
}
|
||||
19
src/proxy/fields/headers.go
Normal file
19
src/proxy/fields/headers.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
func NewHTTPHeaders(headers map[string]string) (http.Header, E.NestedError) {
|
||||
h := make(http.Header)
|
||||
for k, v := range headers {
|
||||
vSplit := strings.Split(v, ",")
|
||||
for _, header := range vSplit {
|
||||
h.Add(k, strings.TrimSpace(header))
|
||||
}
|
||||
}
|
||||
return h, E.Nil()
|
||||
}
|
||||
12
src/proxy/fields/host.go
Normal file
12
src/proxy/fields/host.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type Host string
|
||||
type Subdomain = Alias
|
||||
|
||||
func NewHost(s string) (Host, E.NestedError) {
|
||||
return Host(s), E.Nil()
|
||||
}
|
||||
24
src/proxy/fields/path_mode.go
Normal file
24
src/proxy/fields/path_mode.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type PathMode string
|
||||
|
||||
func NewPathMode(pm string) (PathMode, E.NestedError) {
|
||||
switch pm {
|
||||
case "", "forward":
|
||||
return PathMode(pm), E.Nil()
|
||||
default:
|
||||
return "", E.Invalid("path mode", pm)
|
||||
}
|
||||
}
|
||||
|
||||
func (p PathMode) IsRemove() bool {
|
||||
return p == ""
|
||||
}
|
||||
|
||||
func (p PathMode) IsForward() bool {
|
||||
return p == "forward"
|
||||
}
|
||||
37
src/proxy/fields/path_pattern.go
Normal file
37
src/proxy/fields/path_pattern.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type PathPattern string
|
||||
type PathPatterns = []PathPattern
|
||||
|
||||
func NewPathPattern(s string) (PathPattern, E.NestedError) {
|
||||
if len(s) == 0 {
|
||||
return "", E.Invalid("path", "must not be empty")
|
||||
}
|
||||
if !pathPattern.MatchString(string(s)) {
|
||||
return "", E.Invalid("path pattern", s)
|
||||
}
|
||||
return PathPattern(s), E.Nil()
|
||||
}
|
||||
|
||||
func NewPathPatterns(s []string) (PathPatterns, E.NestedError) {
|
||||
if len(s) == 0 {
|
||||
return []PathPattern{"/"}, E.Nil()
|
||||
}
|
||||
pp := make(PathPatterns, len(s))
|
||||
for i, v := range s {
|
||||
if pattern, err := NewPathPattern(v); err.IsNotNil() {
|
||||
return nil, err
|
||||
} else {
|
||||
pp[i] = pattern
|
||||
}
|
||||
}
|
||||
return pp, E.Nil()
|
||||
}
|
||||
|
||||
var pathPattern = regexp.MustCompile("^((GET|POST|DELETE|PUT|PATCH|HEAD|OPTIONS|CONNECT)\\s)?(/\\w*)+/?$")
|
||||
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.Invalid("port number", v).With(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)
|
||||
)
|
||||
36
src/proxy/fields/scheme.go
Normal file
36
src/proxy/fields/scheme.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type Scheme string
|
||||
|
||||
func NewScheme(s string) (Scheme, E.NestedError) {
|
||||
switch s {
|
||||
case "http", "https", "tcp", "udp":
|
||||
return Scheme(s), E.Nil()
|
||||
}
|
||||
return "", 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(s), E.Nil()
|
||||
}
|
||||
|
||||
func (s Scheme) IsHTTP() bool { return s == "http" }
|
||||
func (s Scheme) IsHTTPS() bool { return s == "https" }
|
||||
func (s Scheme) IsTCP() bool { return s == "tcp" }
|
||||
func (s Scheme) IsUDP() bool { return s == "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).With("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()
|
||||
}
|
||||
43
src/proxy/fields/stream_scheme.go
Normal file
43
src/proxy/fields/stream_scheme.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 fmt.Sprintf("%s -> %s", s.ListeningScheme, s.ProxyScheme)
|
||||
}
|
||||
|
||||
// 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 = "*"
|
||||
150
src/proxy/provider/docker_provider.go
Executable file
150
src/proxy/provider/docker_provider.go
Executable file
@@ -0,0 +1,150 @@
|
||||
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(dockerHost string) ProviderImpl {
|
||||
return &DockerProvider{dockerHost: dockerHost}
|
||||
}
|
||||
|
||||
// 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: (non-nil) 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) {
|
||||
entries := M.NewProxyEntries()
|
||||
|
||||
info, err := D.GetClientInfo(p.dockerHost)
|
||||
if err.IsNotNil() {
|
||||
return entries, E.From(err)
|
||||
}
|
||||
|
||||
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(string(a), &M.ProxyEntry{
|
||||
Alias: string(a),
|
||||
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
|
||||
}
|
||||
54
src/proxy/provider/file_provider.go
Normal file
54
src/proxy/provider/file_provider.go
Normal file
@@ -0,0 +1,54 @@
|
||||
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(filename string) ProviderImpl {
|
||||
return &FileProvider{
|
||||
fileName: filename,
|
||||
path: path.Join(common.ConfigBasePath, filename),
|
||||
}
|
||||
}
|
||||
|
||||
func Validate(data []byte) E.NestedError {
|
||||
return U.ValidateYaml(U.GetSchema(common.ProvidersSchemaPath), data)
|
||||
}
|
||||
|
||||
func (p *FileProvider) String() string {
|
||||
return p.fileName
|
||||
}
|
||||
|
||||
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).With(err)
|
||||
}
|
||||
ne := E.Failure("validation").Subject(p)
|
||||
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)
|
||||
}
|
||||
185
src/proxy/provider/provider.go
Normal file
185
src/proxy/provider/provider.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
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
|
||||
t ProviderType
|
||||
routes *R.Routes
|
||||
reloadReqCh chan struct{}
|
||||
|
||||
watcher W.Watcher
|
||||
watcherCtx context.Context
|
||||
watcherCancel context.CancelFunc
|
||||
|
||||
l *logrus.Entry
|
||||
}
|
||||
|
||||
type ProviderType string
|
||||
|
||||
const (
|
||||
ProviderTypeDocker ProviderType = "docker"
|
||||
ProviderTypeFile ProviderType = "file"
|
||||
)
|
||||
|
||||
func newProvider(name string, t ProviderType) *Provider {
|
||||
p := &Provider{
|
||||
name: name,
|
||||
t: t,
|
||||
routes: R.NewRoutes(),
|
||||
reloadReqCh: make(chan struct{}, 1),
|
||||
}
|
||||
p.l = logrus.WithField("provider", p)
|
||||
|
||||
return p
|
||||
}
|
||||
func NewFileProvider(filename string) *Provider {
|
||||
name := path.Base(filename)
|
||||
p := newProvider(name, ProviderTypeFile)
|
||||
p.ProviderImpl = FileProviderImpl(filename)
|
||||
p.watcher = p.NewWatcher()
|
||||
return p
|
||||
}
|
||||
|
||||
func NewDockerProvider(name string, dockerHost string) *Provider {
|
||||
p := newProvider(name, ProviderTypeDocker)
|
||||
p.ProviderImpl = DockerProviderImpl(dockerHost)
|
||||
p.watcher = p.NewWatcher()
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Provider) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *Provider) GetType() ProviderType {
|
||||
return p.t
|
||||
}
|
||||
|
||||
func (p *Provider) String() string {
|
||||
return fmt.Sprintf("%s: %s", p.t, 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()
|
||||
|
||||
errors := E.NewBuilder("errors in routes")
|
||||
nStarted := 0
|
||||
nFailed := 0
|
||||
|
||||
if err.IsNotNil() {
|
||||
errors.Add(err)
|
||||
}
|
||||
|
||||
p.routes.EachKVParallel(func(alias string, r R.Route) {
|
||||
if err := r.Start(); err.IsNotNil() {
|
||||
errors.Add(err.Subject(r))
|
||||
nFailed++
|
||||
} else {
|
||||
nStarted++
|
||||
}
|
||||
})
|
||||
p.l.Infof("%d routes started, %d failed", nStarted, nFailed)
|
||||
return errors.Build()
|
||||
}
|
||||
|
||||
func (p *Provider) StopAllRoutes() E.NestedError {
|
||||
if p.watcherCancel != nil {
|
||||
p.watcherCancel()
|
||||
p.watcherCancel = nil
|
||||
}
|
||||
errors := E.NewBuilder("errors stopping routes for provider %q", p.name)
|
||||
nStopped := 0
|
||||
nFailed := 0
|
||||
p.routes.EachKVParallel(func(alias string, r R.Route) {
|
||||
if err := r.Stop(); err.IsNotNil() {
|
||||
errors.Add(err.Subject(r))
|
||||
nFailed++
|
||||
} else {
|
||||
nStopped++
|
||||
}
|
||||
})
|
||||
p.l.Infof("%d routes stopped, %d failed", nStopped, nFailed)
|
||||
return errors.Build()
|
||||
}
|
||||
|
||||
func (p *Provider) ReloadRoutes() {
|
||||
defer p.l.Info("routes reloaded")
|
||||
|
||||
p.StopAllRoutes()
|
||||
p.loadRoutes()
|
||||
p.StartAllRoutes()
|
||||
}
|
||||
|
||||
func (p *Provider) GetCurrentRoutes() *R.Routes {
|
||||
return p.routes
|
||||
}
|
||||
|
||||
func (p *Provider) watchEvents() {
|
||||
events, errs := p.watcher.Events(p.watcherCtx)
|
||||
l := p.l.WithField("module", "watcher")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.reloadReqCh: // block until last reload is done
|
||||
p.ReloadRoutes()
|
||||
continue // ignore events once after reload
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
l.Info(event)
|
||||
p.reloadReqCh <- struct{}{}
|
||||
case err, ok := <-errs:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err.Is(context.Canceled) {
|
||||
continue
|
||||
}
|
||||
l.Errorf("watcher error: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) loadRoutes() E.NestedError {
|
||||
entries, err := p.GetProxyEntries()
|
||||
|
||||
if err.IsNotNil() {
|
||||
p.l.Warn(err.Subject(p))
|
||||
}
|
||||
p.routes = R.NewRoutes()
|
||||
|
||||
errors := E.NewBuilder("errors loading routes from %s", p)
|
||||
entries.EachKV(func(a string, e *M.ProxyEntry) {
|
||||
e.Alias = a
|
||||
r, err := R.NewRoute(e)
|
||||
if err.IsNotNil() {
|
||||
errors.Add(err.Subject(a))
|
||||
} else {
|
||||
p.routes.Set(a, r)
|
||||
}
|
||||
})
|
||||
return errors.Build()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user