mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-12 05:20:33 +01:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99216ffe59 | ||
|
|
f426dbc9cf | ||
|
|
1c611cc9b9 | ||
|
|
dc43e26770 | ||
|
|
79ae26f1b5 | ||
|
|
109c2460fa | ||
|
|
71e8e4a462 | ||
|
|
8e2cc56afb | ||
|
|
6728bc39d2 | ||
|
|
daca4b7735 | ||
|
|
3b597eea29 | ||
|
|
090b73d287 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -15,4 +15,6 @@ go.work.sum
|
||||
|
||||
!src/**/
|
||||
|
||||
todo.md
|
||||
todo.md
|
||||
|
||||
.*.swp
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,4 +1,5 @@
|
||||
FROM golang:1.23.1-alpine AS builder
|
||||
RUN apk add --no-cache tzdata
|
||||
COPY src /src
|
||||
ENV GOCACHE=/root/.cache/go-build
|
||||
WORKDIR /src
|
||||
@@ -7,16 +8,20 @@ RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||
go mod download && \
|
||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o go-proxy github.com/yusing/go-proxy
|
||||
|
||||
FROM alpine:3.20
|
||||
FROM scratch
|
||||
|
||||
LABEL maintainer="yusing@6uo.me"
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
# copy timezone data
|
||||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
|
||||
# copy binary
|
||||
COPY --from=builder /src/go-proxy /app/
|
||||
COPY schema/ /app/schema
|
||||
|
||||
RUN chmod +x /app/go-proxy
|
||||
# copy cert required for setup
|
||||
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||
|
||||
ENV DOCKER_HOST=unix:///var/run/docker.sock
|
||||
ENV GOPROXY_DEBUG=0
|
||||
|
||||
|
||||
85
README.md
85
README.md
@@ -24,22 +24,24 @@ A lightweight, easy-to-use, and [performant](docs/benchmark_result.md) reverse p
|
||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
||||
- [Config File](#config-file)
|
||||
- [Provider File](#provider-file)
|
||||
- [Known issues](#known-issues)
|
||||
- [Showcase](#showcase)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
- [Build it yourself](#build-it-yourself)
|
||||
|
||||
## Key Points
|
||||
|
||||
- Easy to use
|
||||
- Effortless configuration
|
||||
- Error messages is clear and detailed, easy troubleshooting
|
||||
- Auto certificate obtaining and renewal (See [Supported DNS Challenge Providers](docs/dns_providers.md))
|
||||
- Auto configuration for docker containers
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- Stop containers on idle, wake it up on traffic _(optional)_
|
||||
- HTTP(s) reserve proxy
|
||||
- TCP and UDP port forwarding
|
||||
- Web UI for configuration and monitoring (See [screenshots](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
||||
- Written in **[Go](https://go.dev)**
|
||||
- Easy to use
|
||||
- Effortless configuration
|
||||
- Simple multi-node setup
|
||||
- Error messages is clear and detailed, easy troubleshooting
|
||||
- Auto certificate obtaining and renewal (See [Supported DNS Challenge Providers](docs/dns_providers.md))
|
||||
- Auto configuration for docker containers
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- **idlesleeper**: stop containers on idle, wake it up on traffic _(optional, see [showcase](#idlesleeper))_
|
||||
- HTTP(s) reserve proxy
|
||||
- TCP and UDP port forwarding
|
||||
- Web UI for configuration and monitoring (See [screenshots](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
||||
- Written in **[Go](https://go.dev)**
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -47,18 +49,29 @@ A lightweight, easy-to-use, and [performant](docs/benchmark_result.md) reverse p
|
||||
|
||||
### Setup
|
||||
|
||||
1. Setup DNS Records, e.g.
|
||||
1. Pull docker image
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/yusing/go-proxy:latest
|
||||
```
|
||||
|
||||
- A Record: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||
2. Create new directory, `cd` into it, then run setup
|
||||
|
||||
2. Setup `go-proxy` [See here](docs/docker.md)
|
||||
```shell
|
||||
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/go-proxy setup
|
||||
```
|
||||
|
||||
3. Setup `docker-socket-proxy` (see [example](docs/docker_socket_proxy.md) other machine that is running docker (if any)
|
||||
3. Setup DNS Records point to machine which runs `go-proxy`, e.g.
|
||||
|
||||
4. Configure `go-proxy`
|
||||
- with text editor (e.g. Visual Studio Code)
|
||||
- or with web config editor via `http://gp.y.z`
|
||||
- A Record: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||
|
||||
4. Setup `docker-socket-proxy` other docker nodes _(if any)_ (see [example](docs/docker_socket_proxy.md)) and then them inside `config.yml`
|
||||
|
||||
5. Done. You may now do some extra configuration
|
||||
- With text editor (e.g. Visual Studio Code)
|
||||
- With Web UI via `gp.y.z`
|
||||
- For more info, [See docker.md](docs/docker.md)
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -97,21 +110,21 @@ 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
|
||||
- ...
|
||||
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
|
||||
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)
|
||||
@@ -124,9 +137,11 @@ See [providers.example.yml](providers.example.yml) for examples
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Known issues
|
||||
## Showcase
|
||||
|
||||
- `autocert` config is not hot-reloadable
|
||||
### idlesleeper
|
||||
|
||||

|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
|
||||
@@ -22,22 +22,23 @@
|
||||
- [VSCode 中使用 JSON Schema](#vscode-中使用-json-schema)
|
||||
- [配置文件](#配置文件)
|
||||
- [透過文件配置](#透過文件配置)
|
||||
- [已知問題](#已知問題)
|
||||
- [展示](#展示)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
- [源碼編譯](#源碼編譯)
|
||||
|
||||
## 重點
|
||||
|
||||
- 易用
|
||||
- 不需花費太多時間就能輕鬆配置
|
||||
- 除錯簡單
|
||||
- 自動處理 HTTPS 證書(參見[可用的 DNS 供應商](docs/dns_providers.md))
|
||||
- 透過 Docker 容器自動配置
|
||||
- 容器狀態變更時自動熱重載
|
||||
- 容器閒置時自動暫停/停止,入站時自動喚醒
|
||||
- HTTP(s)反向代理
|
||||
- TCP/UDP 端口轉發
|
||||
- 用於配置和監控的前端 Web 面板([截圖](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
||||
- 使用 **[Go](https://go.dev)** 編寫
|
||||
- 易用
|
||||
- 不需花費太多時間就能輕鬆配置
|
||||
- 除錯簡單
|
||||
- 自動處理 HTTPS 證書(參見[可用的 DNS 供應商](docs/dns_providers.md))
|
||||
- 透過 Docker 容器自動配置
|
||||
- 容器狀態變更時自動熱重載
|
||||
- 容器閒置時自動暫停/停止,入站時自動喚醒
|
||||
- HTTP(s)反向代理
|
||||
- TCP/UDP 端口轉發
|
||||
- 用於配置和監控的前端 Web 面板([截圖](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
||||
- 使用 **[Go](https://go.dev)** 編寫
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
@@ -47,14 +48,14 @@
|
||||
|
||||
1. 設置 DNS 記錄,例如:
|
||||
|
||||
- A 記錄: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA 記錄: `*.y.z` -> `::ffff:a00:a01`
|
||||
- A 記錄: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA 記錄: `*.y.z` -> `::ffff:a00:a01`
|
||||
|
||||
2. 安裝 `go-proxy` [參見這裡](docs/docker.md)
|
||||
|
||||
3. 配置 `go-proxy`
|
||||
- 使用文本編輯器 (推薦 Visual Studio Code [參見 VSCode 使用 schema](#vscode-中使用-json-schema))
|
||||
- 或通過 `http://gp.y.z` 使用網頁配置編輯器
|
||||
- 使用文本編輯器 (推薦 Visual Studio Code [參見 VSCode 使用 schema](#vscode-中使用-json-schema))
|
||||
- 或通過 `http://gp.y.z` 使用網頁配置編輯器
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
@@ -93,21 +94,21 @@
|
||||
```yaml
|
||||
# autocert 配置
|
||||
autocert:
|
||||
email: # ACME 電子郵件
|
||||
domains: # 域名列表
|
||||
provider: # DNS 供應商
|
||||
options: # 供應商個別配置
|
||||
- ...
|
||||
email: # ACME 電子郵件
|
||||
domains: # 域名列表
|
||||
provider: # DNS 供應商
|
||||
options: # 供應商個別配置
|
||||
- ...
|
||||
# 配置文件 / docker
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
@@ -120,9 +121,11 @@ providers:
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
## 已知問題
|
||||
## 展示
|
||||
|
||||
- `autocert` 配置不能熱重載
|
||||
### idlesleeper
|
||||
|
||||

|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
# options:
|
||||
# - auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
|
||||
# 3. other providers, check readme for more
|
||||
# 3. other providers, check docs/dns_providers.md for more
|
||||
|
||||
providers:
|
||||
include:
|
||||
- providers.yml # config/providers.yml
|
||||
# add some more below if you want
|
||||
# - file1.yml # config/file_1.yml
|
||||
# - file2.yml
|
||||
# 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/
|
||||
# $DOCKER_HOST implies unix:///var/run/docker.sock by default
|
||||
@@ -30,8 +30,7 @@ providers:
|
||||
# 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
|
||||
# redirect_to_https: false # redirect http requests to https (if enabled)
|
||||
|
||||
@@ -11,42 +11,70 @@
|
||||
|
||||
## Cloudflare
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
provider: cloudflare
|
||||
options:
|
||||
auth_token:
|
||||
```
|
||||
|
||||
`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`
|
||||
```yaml
|
||||
autocert:
|
||||
provider: clouddns
|
||||
options:
|
||||
client_id:
|
||||
email:
|
||||
password:
|
||||
```
|
||||
|
||||
## DuckDNS
|
||||
|
||||
- `token`: DuckDNS Token
|
||||
```yaml
|
||||
autocert:
|
||||
provider: duckdns
|
||||
options:
|
||||
token:
|
||||
```
|
||||
|
||||
Tested by [earvingad](https://github.com/earvingad)
|
||||
|
||||
## OVHCloud
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
provider: ovh
|
||||
options:
|
||||
api_endpoint:
|
||||
application_key:
|
||||
application_secret:
|
||||
consumer_key:
|
||||
oauth2_config:
|
||||
client_id:
|
||||
client_secret:
|
||||
```
|
||||
|
||||
_Note, `application_key` and `oauth2_config` **CANNOT** be used together_
|
||||
|
||||
- `api_endpoint`: Endpoint URL, or one of
|
||||
- `ovh-eu`,
|
||||
- `ovh-ca`,
|
||||
- `ovh-us`,
|
||||
- `kimsufi-eu`,
|
||||
- `kimsufi-ca`,
|
||||
- `soyoustart-eu`,
|
||||
- `soyoustart-ca`
|
||||
- `application_secret`
|
||||
- `application_key`
|
||||
- `consumer_key`
|
||||
- `oauth2_config`: Client ID and Client Secret
|
||||
- `client_id`
|
||||
- `client_secret`
|
||||
- `api_endpoint`: Endpoint URL, or one of
|
||||
- `ovh-eu`,
|
||||
- `ovh-ca`,
|
||||
- `ovh-us`,
|
||||
- `kimsufi-eu`,
|
||||
- `kimsufi-ca`,
|
||||
- `soyoustart-eu`,
|
||||
- `soyoustart-ca`
|
||||
- `application_secret`
|
||||
- `application_key`
|
||||
- `consumer_key`
|
||||
- `oauth2_config`: Client ID and Client Secret
|
||||
- `client_id`
|
||||
- `client_secret`
|
||||
|
||||
## Implement other DNS providers
|
||||
|
||||
|
||||
339
docs/docker.md
339
docs/docker.md
@@ -6,7 +6,7 @@
|
||||
|
||||
- [Docker compose guide](#docker-compose-guide)
|
||||
- [Table of content](#table-of-content)
|
||||
- [Setup](#setup)
|
||||
- [Additional setup](#additional-setup)
|
||||
- [Labels](#labels)
|
||||
- [Syntax](#syntax)
|
||||
- [Fields](#fields)
|
||||
@@ -16,34 +16,11 @@
|
||||
- [Docker compose examples](#docker-compose-examples)
|
||||
- [Services URLs for above examples](#services-urls-for-above-examples)
|
||||
|
||||
## Setup
|
||||
## Additional setup
|
||||
|
||||
1. Install `wget` if not already
|
||||
1. Enable HTTPs _(optional)_
|
||||
|
||||
- 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)
|
||||
Mount a folder to store obtained certs or to load existing cert
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@@ -57,27 +34,28 @@
|
||||
|
||||
```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
|
||||
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
|
||||
provider: local
|
||||
cert_path: /app/certs/cert.crt
|
||||
key_path: /app/certs/priv.key
|
||||
```
|
||||
|
||||
5. Modify `compose.yml` to fit your needs
|
||||
2. Modify `compose.yml` to fit your needs
|
||||
|
||||
6. Run `docker compose up -d` to start the container
|
||||
3. 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
|
||||
4. 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)
|
||||
|
||||
@@ -85,31 +63,31 @@
|
||||
|
||||
### Syntax
|
||||
|
||||
| Label | Description | Default | Accepted values |
|
||||
| ------------------------ | --------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------- |
|
||||
| `proxy.aliases` | comma separated aliases for subdomain and label matching | `container_name` | any |
|
||||
| `proxy.exclude` | to be excluded from `go-proxy` | false | boolean |
|
||||
| `proxy.idle_timeout` | time for idle (no traffic) before put it into sleep **(http/s only)** | empty **(disabled)** | `number[unit]...`, e.g. `1m30s` |
|
||||
| `proxy.wake_timeout` | time to wait for container to start before responding a loading page | empty | `number[unit]...` |
|
||||
| `proxy.stop_method` | method to stop after `idle_timeout` | `stop` | `stop`, `pause`, `kill` |
|
||||
| `proxy.stop_timeout` | time to wait for stop command | `10s` | `number[unit]...` |
|
||||
| `proxy.stop_signal` | signal sent to container for `stop` and `kill` methods | docker's default | `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT` and those without **SIG** prefix |
|
||||
| `proxy.<alias>.<field>` | set field for specific alias | N/A | N/A |
|
||||
| `proxy.$<index>.<field>` | set field for specific alias at index (starting from **1**) | N/A | N/A |
|
||||
| `proxy.*.<field>` | set field for all aliases | N/A | N/A |
|
||||
| Label | Description | Example | Default | Accepted values |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | --------------------------- | ------------------------------------------------------------------------- |
|
||||
| `proxy.aliases` | comma separated aliases for subdomain and label matching | `gitlab,gitlab-reg,gitlab-ssh` | `container_name` | any |
|
||||
| `proxy.exclude` | to be excluded from `go-proxy` | | false | boolean |
|
||||
| `proxy.idle_timeout` | time for idle (no traffic) before put it into sleep **(http/s only)**<br> _**NOTE: idlewatcher will only be enabled containers that has non-empty `idle_timeout`**_ | `1h` | empty or `0` **(disabled)** | `number[unit]...`, e.g. `1m30s` |
|
||||
| `proxy.wake_timeout` | time to wait for target site to be ready | | `10s` | `number[unit]...` |
|
||||
| `proxy.stop_method` | method to stop after `idle_timeout` | | `stop` | `stop`, `pause`, `kill` |
|
||||
| `proxy.stop_timeout` | time to wait for stop command | | `10s` | `number[unit]...` |
|
||||
| `proxy.stop_signal` | signal sent to container for `stop` and `kill` methods | | docker's default | `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT` and those without **SIG** prefix |
|
||||
| `proxy.<alias>.<field>` | set field for specific alias | `proxy.gitlab-ssh.scheme` | N/A | N/A |
|
||||
| `proxy.$<index>.<field>` | set field for specific alias at index (starting from **1**) | `proxy.$3.port` | N/A | N/A |
|
||||
| `proxy.*.<field>` | set field for all aliases | `proxy.*.set_headers` | N/A | N/A |
|
||||
|
||||
### 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: docker client IP / hostname </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 |
|
||||
| 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: docker client IP / hostname </li><li>File: `localhost`</li></ul> | IP address, hostname |
|
||||
| `port` | proxy port **(http/s)** | first port returned from docker | number in range of `1 - 65535` |
|
||||
| `port` | proxy port **(tcp/udp)** | `0:first_port` | `x:y` <br><ul><li>**x**: port for `go-proxy` to listen on.<br>**x** can be 0, which means listen on a random port</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 | `/` **(proxy all requests)** | yaml style list[<sup>1</sup>](#list-example) of ([path patterns](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 |
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -134,11 +112,11 @@ 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
|
||||
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
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
@@ -164,52 +142,52 @@ File Provider
|
||||
|
||||
```yaml
|
||||
service_a:
|
||||
host: service_a.internal
|
||||
path_patterns:
|
||||
- GET /
|
||||
- POST /auth
|
||||
hide_headers:
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
host: service_a.internal
|
||||
path_patterns:
|
||||
- GET /
|
||||
- POST /auth
|
||||
hide_headers:
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Container not showing up in proxies list
|
||||
- Container not showing up in proxies list
|
||||
|
||||
Please check that either `ports` or label `proxy.<alias>.port` is declared, e.g.
|
||||
Please check that either `ports` or label `proxy.<alias>.port` is declared, e.g.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx-1: # Option 1
|
||||
...
|
||||
ports:
|
||||
- 80
|
||||
nginx-2: # Option 2
|
||||
...
|
||||
container_name: nginx-2
|
||||
network_mode: host
|
||||
labels:
|
||||
proxy.nginx-2.port: 80
|
||||
```
|
||||
```yaml
|
||||
services:
|
||||
nginx-1: # Option 1
|
||||
...
|
||||
ports:
|
||||
- 80
|
||||
nginx-2: # Option 2
|
||||
...
|
||||
container_name: nginx-2
|
||||
network_mode: host
|
||||
labels:
|
||||
proxy.nginx-2.port: 80
|
||||
```
|
||||
|
||||
- Firewall issues
|
||||
- Firewall issues
|
||||
|
||||
If you are using `ufw` with vpn that drop all inbound traffic except vpn, run below:
|
||||
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`
|
||||
`sudo ufw allow from 172.16.0.0/16 to 100.64.0.0/10`
|
||||
|
||||
Explaination:
|
||||
Explaination:
|
||||
|
||||
Docker network is usually `172.16.0.0/16`
|
||||
Docker network is usually `172.16.0.0/16`
|
||||
|
||||
Tailscale is used as an example, `100.64.0.0/10` will be the CIDR
|
||||
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:
|
||||
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' -`
|
||||
`docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -`
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -219,98 +197,97 @@ More examples in [here](examples/)
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
adg-work:
|
||||
adg-conf:
|
||||
mc-data:
|
||||
palworld:
|
||||
nginx:
|
||||
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.$1.port=80
|
||||
- proxy.$2.scheme=udp
|
||||
- proxy.$2.port=20000:dns
|
||||
- proxy.$3.port=3000
|
||||
volumes:
|
||||
- adg-work:/opt/adguardhome/work
|
||||
- adg-conf:/opt/adguardhome/conf
|
||||
ports:
|
||||
- 80
|
||||
- 3000
|
||||
- 53/udp
|
||||
mc:
|
||||
image: itzg/minecraft-server
|
||||
tty: true
|
||||
stdin_open: true
|
||||
container_name: mc
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 25565
|
||||
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
|
||||
ports:
|
||||
- 8211/udp
|
||||
- 27015/udp
|
||||
labels:
|
||||
- proxy.aliases=pal1,pal2
|
||||
- proxy.*.scheme=udp
|
||||
- proxy.$1.port=20002:8211
|
||||
- proxy.$2.port=20003:27015
|
||||
environment: ...
|
||||
volumes:
|
||||
- palworld:/palworld
|
||||
nginx:
|
||||
image: nginx
|
||||
container_name: nginx
|
||||
volumes:
|
||||
- nginx:/usr/share/nginx/html
|
||||
ports:
|
||||
- 80
|
||||
labels:
|
||||
proxy.idle_timeout: 1m
|
||||
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
|
||||
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
|
||||
- proxy.gp.port=3000
|
||||
depends_on:
|
||||
- go-proxy
|
||||
adg:
|
||||
image: adguard/adguardhome
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- proxy.aliases=adg,adg-dns,adg-setup
|
||||
- proxy.$1.port=80
|
||||
- proxy.$2.scheme=udp
|
||||
- proxy.$2.port=20000:dns
|
||||
- proxy.$3.port=3000
|
||||
volumes:
|
||||
- adg-work:/opt/adguardhome/work
|
||||
- adg-conf:/opt/adguardhome/conf
|
||||
ports:
|
||||
- 80
|
||||
- 3000
|
||||
- 53/udp
|
||||
mc:
|
||||
image: itzg/minecraft-server
|
||||
tty: true
|
||||
stdin_open: true
|
||||
container_name: mc
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 25565
|
||||
labels:
|
||||
- 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
|
||||
ports:
|
||||
- 8211/udp
|
||||
- 27015/udp
|
||||
labels:
|
||||
- proxy.aliases=pal1,pal2
|
||||
- proxy.*.scheme=udp
|
||||
- proxy.$1.port=20002:8211
|
||||
- proxy.$2.port=20003:27015
|
||||
environment: ...
|
||||
volumes:
|
||||
- palworld:/palworld
|
||||
nginx:
|
||||
image: nginx
|
||||
container_name: nginx
|
||||
volumes:
|
||||
- nginx:/usr/share/nginx/html
|
||||
ports:
|
||||
- 80
|
||||
labels:
|
||||
proxy.idle_timeout: 1m
|
||||
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
|
||||
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
|
||||
- proxy.gp.port=3000
|
||||
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:2000`: adguard dns (udp)
|
||||
- `yourdomain.com:20001`: minecraft server
|
||||
- `yourdomain.com:20002`: palworld server
|
||||
- `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:2000`: adguard dns (udp)
|
||||
- `yourdomain.com:20001`: minecraft server
|
||||
- `yourdomain.com:20002`: palworld server
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -4,56 +4,37 @@ For docker client on other machine, set this up, then add `name: tcp://<machine_
|
||||
|
||||
```yml
|
||||
# compose.yml on remote machine (e.g. server1)
|
||||
services:
|
||||
docker-proxy:
|
||||
container_name: docker-proxy
|
||||
image: ghcr.io/linuxserver/socket-proxy
|
||||
environment:
|
||||
- ALLOW_START=1 #optional
|
||||
- ALLOW_STOP=1 #optional
|
||||
- ALLOW_RESTARTS=0 #optional
|
||||
- AUTH=0 #optional
|
||||
- BUILD=0 #optional
|
||||
- COMMIT=0 #optional
|
||||
- CONFIGS=0 #optional
|
||||
- CONTAINERS=1 #optional
|
||||
- DISABLE_IPV6=1 #optional
|
||||
- DISTRIBUTION=0 #optional
|
||||
- EVENTS=1 #optional
|
||||
- EXEC=0 #optional
|
||||
- IMAGES=0 #optional
|
||||
- INFO=0 #optional
|
||||
- NETWORKS=0 #optional
|
||||
- NODES=0 #optional
|
||||
- PING=1 #optional
|
||||
- POST=1 #optional
|
||||
- PLUGINS=0 #optional
|
||||
- SECRETS=0 #optional
|
||||
- SERVICES=0 #optional
|
||||
- SESSION=0 #optional
|
||||
- SWARM=0 #optional
|
||||
- SYSTEM=0 #optional
|
||||
- TASKS=0 #optional
|
||||
- VERSION=1 #optional
|
||||
- VOLUMES=0 #optional
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
restart: always
|
||||
tmpfs:
|
||||
- /run
|
||||
ports:
|
||||
- 2375:2375
|
||||
docker-proxy:
|
||||
container_name: docker-proxy
|
||||
image: tecnativa/docker-socket-proxy
|
||||
privileged: true
|
||||
environment:
|
||||
- ALLOW_START=1
|
||||
- ALLOW_STOP=1
|
||||
- ALLOW_RESTARTS=1
|
||||
- CONTAINERS=1
|
||||
- EVENTS=1
|
||||
- PING=1
|
||||
- POST=1
|
||||
- VERSION=1
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
restart: always
|
||||
ports:
|
||||
- 2375:2375
|
||||
# or more secure
|
||||
- <machine_private_ip>:2375:2375
|
||||
```
|
||||
|
||||
```yml
|
||||
# config.yml on go-proxy machine
|
||||
autocert:
|
||||
... # your config
|
||||
... # your config
|
||||
|
||||
providers:
|
||||
include:
|
||||
...
|
||||
docker:
|
||||
...
|
||||
server1: tcp://<machine_ip>:2375
|
||||
include:
|
||||
...
|
||||
docker:
|
||||
...
|
||||
server1: tcp://<machine_ip>:2375
|
||||
```
|
||||
|
||||
2
frontend
2
frontend
Submodule frontend updated: d0e59630d6...441fd708db
@@ -1,22 +1,21 @@
|
||||
example: # matching `app.y.z`
|
||||
scheme: https
|
||||
host: 10.0.0.1
|
||||
port: 80
|
||||
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
|
||||
example: # matching `example.y.z`
|
||||
scheme: https
|
||||
host: 10.0.0.1
|
||||
port: 80
|
||||
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/{$} # for exactly /home
|
||||
no_tls_verify: false
|
||||
set_headers:
|
||||
HEADER_A: VALUE_A, VALUE_B
|
||||
HEADER_B: VALUE_C
|
||||
hide_headers:
|
||||
- HEADER_C
|
||||
- HEADER_D
|
||||
app1: # app1 -> localhost:8080
|
||||
port: 8080
|
||||
app2:
|
||||
scheme: tcp
|
||||
host: 10.0.0.2
|
||||
port: 20000:tcp
|
||||
scheme: udp
|
||||
host: 10.0.0.2
|
||||
port: 2223:dns
|
||||
|
||||
@@ -75,7 +75,10 @@
|
||||
"scheme": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": ["http", "https"]
|
||||
"enum": [
|
||||
"http",
|
||||
"https"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -87,7 +90,7 @@
|
||||
"then": {
|
||||
"properties": {
|
||||
"port": {
|
||||
"markdownDescription": "Proxy port from **1** to **65535**",
|
||||
"markdownDescription": "Proxy port from **0** to **65535**",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -96,7 +99,7 @@
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"maximum": 65535
|
||||
}
|
||||
]
|
||||
@@ -160,7 +163,9 @@
|
||||
"not": true
|
||||
}
|
||||
},
|
||||
"required": ["port"]
|
||||
"required": [
|
||||
"port"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -192,4 +197,4 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
if [ -z "$BRANCH" ]; then
|
||||
BRANCH="v0.5"
|
||||
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
|
||||
BIN
showcase/idlesleeper.webp
Normal file
BIN
showcase/idlesleeper.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 831 KiB |
@@ -59,7 +59,7 @@ func (p *Provider) ObtainCert() (res E.NestedError) {
|
||||
defer b.To(&res)
|
||||
|
||||
if p.cfg.Provider == ProviderLocal {
|
||||
b.Addf("provider is set to %q", ProviderLocal)
|
||||
b.Addf("provider is set to %q", ProviderLocal).WithSeverity(E.SeverityWarning)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,11 +71,9 @@ func (p *Provider) ObtainCert() (res E.NestedError) {
|
||||
}
|
||||
|
||||
if p.user.Registration == nil {
|
||||
if err := p.loadRegistration(); err.HasError() {
|
||||
if err := p.registerACME(); err.HasError() {
|
||||
b.Add(E.FailWith("register ACME", err))
|
||||
return
|
||||
}
|
||||
if err := p.registerACME(); err.HasError() {
|
||||
b.Add(E.FailWith("register ACME", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,16 +87,18 @@ func (p *Provider) ObtainCert() (res E.NestedError) {
|
||||
b.Add(err)
|
||||
return
|
||||
}
|
||||
err = p.saveCert(cert)
|
||||
if err.HasError() {
|
||||
|
||||
if err = p.saveCert(cert); err.HasError() {
|
||||
b.Add(E.FailWith("save certificate", err))
|
||||
return
|
||||
}
|
||||
|
||||
tlsCert, err := E.Check(tls.X509KeyPair(cert.Certificate, cert.PrivateKey))
|
||||
if err.HasError() {
|
||||
b.Add(E.FailWith("parse obtained certificate", err))
|
||||
return
|
||||
}
|
||||
|
||||
expiries, err := getCertExpiries(&tlsCert)
|
||||
if err.HasError() {
|
||||
b.Add(E.FailWith("get certificate expiry", err))
|
||||
@@ -187,29 +187,9 @@ func (p *Provider) registerACME() E.NestedError {
|
||||
}
|
||||
p.user.Registration = reg
|
||||
|
||||
if err := p.saveRegistration(); err.HasError() {
|
||||
logger.Warn(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) loadRegistration() E.NestedError {
|
||||
if p.user.Registration != nil {
|
||||
return nil
|
||||
}
|
||||
reg := ®istration.Resource{}
|
||||
err := U.LoadJson(RegistrationFile, reg)
|
||||
if err.HasError() {
|
||||
return E.FailWith("parse registration file", err)
|
||||
}
|
||||
p.user.Registration = reg
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) saveRegistration() E.NestedError {
|
||||
return U.SaveJson(RegistrationFile, p.user.Registration, 0o600)
|
||||
}
|
||||
|
||||
func (p *Provider) saveCert(cert *certificate.Resource) E.NestedError {
|
||||
err := os.WriteFile(p.cfg.KeyPath, cert.PrivateKey, 0o600) // -rw-------
|
||||
if err != nil {
|
||||
|
||||
29
src/autocert/setup.go
Normal file
29
src/autocert/setup.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
func (p *Provider) Setup(ctx context.Context) (err E.NestedError) {
|
||||
if err = p.LoadCert(); err != nil {
|
||||
if !err.Is(os.ErrNotExist) { // ignore if cert doesn't exist
|
||||
return err
|
||||
}
|
||||
logger.Debug("obtaining cert due to error loading cert")
|
||||
if err = p.ObtainCert(); err != nil {
|
||||
return err.Warn()
|
||||
}
|
||||
}
|
||||
|
||||
go p.ScheduleRenewal(ctx)
|
||||
|
||||
for _, expiry := range p.GetExpiries() {
|
||||
logger.Infof("certificate expire on %s", expiry)
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -12,21 +12,25 @@ type Args struct {
|
||||
}
|
||||
|
||||
const (
|
||||
CommandStart = ""
|
||||
CommandValidate = "validate"
|
||||
CommandListConfigs = "ls-config"
|
||||
CommandListRoutes = "ls-routes"
|
||||
CommandReload = "reload"
|
||||
CommandDebugListEntries = "debug-ls-entries"
|
||||
CommandStart = ""
|
||||
CommandSetup = "setup"
|
||||
CommandValidate = "validate"
|
||||
CommandListConfigs = "ls-config"
|
||||
CommandListRoutes = "ls-routes"
|
||||
CommandReload = "reload"
|
||||
CommandDebugListEntries = "debug-ls-entries"
|
||||
CommandDebugListProviders = "debug-ls-providers"
|
||||
)
|
||||
|
||||
var ValidCommands = []string{
|
||||
CommandStart,
|
||||
CommandSetup,
|
||||
CommandValidate,
|
||||
CommandListConfigs,
|
||||
CommandListRoutes,
|
||||
CommandReload,
|
||||
CommandDebugListEntries,
|
||||
CommandDebugListProviders,
|
||||
}
|
||||
|
||||
func GetArgs() Args {
|
||||
|
||||
@@ -10,80 +10,28 @@ const (
|
||||
KeepAlive = 5 * time.Second
|
||||
)
|
||||
|
||||
const (
|
||||
ProviderKind_Docker = "docker"
|
||||
ProviderKind_File = "file"
|
||||
)
|
||||
|
||||
// file, folder structure
|
||||
|
||||
const (
|
||||
ConfigBasePath = "config/"
|
||||
ConfigFileName = "config.yml"
|
||||
ConfigPath = ConfigBasePath + ConfigFileName
|
||||
ConfigBasePath = "config"
|
||||
ConfigFileName = "config.yml"
|
||||
ConfigExampleFileName = "config.example.yml"
|
||||
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
||||
)
|
||||
|
||||
const (
|
||||
TemplatesBasePath = "templates/"
|
||||
PanelTemplatePath = TemplatesBasePath + "panel/index.html"
|
||||
ConfigEditorTemplatePath = TemplatesBasePath + "config_editor/index.html"
|
||||
SchemaBasePath = "schema"
|
||||
ConfigSchemaPath = SchemaBasePath + "/config.schema.json"
|
||||
FileProviderSchemaPath = SchemaBasePath + "/providers.schema.json"
|
||||
)
|
||||
|
||||
const (
|
||||
SchemaBasePath = "schema/"
|
||||
ConfigSchemaPath = SchemaBasePath + "config.schema.json"
|
||||
ProvidersSchemaPath = SchemaBasePath + "providers.schema.json"
|
||||
ComposeFileName = "compose.yml"
|
||||
ComposeExampleFileName = "compose.example.yml"
|
||||
)
|
||||
|
||||
const DockerHostFromEnv = "$DOCKER_HOST"
|
||||
|
||||
var WellKnownHTTPPorts = map[uint16]bool{
|
||||
80: true,
|
||||
8000: true,
|
||||
8008: true,
|
||||
8080: true,
|
||||
3000: true,
|
||||
}
|
||||
|
||||
var (
|
||||
ServiceNamePortMapTCP = map[string]int{
|
||||
"postgres": 5432,
|
||||
"mysql": 3306,
|
||||
"mariadb": 3306,
|
||||
"redis": 6379,
|
||||
"mssql": 1433,
|
||||
"memcached": 11211,
|
||||
"rabbitmq": 5672,
|
||||
"mongo": 27017,
|
||||
"minecraft-server": 25565,
|
||||
|
||||
"dns": 53,
|
||||
"ssh": 22,
|
||||
"ftp": 21,
|
||||
"smtp": 25,
|
||||
"pop3": 110,
|
||||
"imap": 143,
|
||||
}
|
||||
)
|
||||
|
||||
var ImageNamePortMapHTTP = map[string]int{
|
||||
"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,
|
||||
}
|
||||
|
||||
const (
|
||||
IdleTimeoutDefault = "0"
|
||||
WakeTimeoutDefault = "10s"
|
||||
|
||||
@@ -7,18 +7,18 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
NoSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION")
|
||||
IsDebug = getEnvBool("GOPROXY_DEBUG")
|
||||
ProxyHTTPAddr = getEnv("GOPROXY_HTTP_ADDR", ":80")
|
||||
ProxyHTTPSAddr = getEnv("GOPROXY_HTTPS_ADDR", ":443")
|
||||
APIHTTPAddr = getEnv("GOPROXY_API_ADDR", "127.0.0.1:8888")
|
||||
NoSchemaValidation = GetEnvBool("GOPROXY_NO_SCHEMA_VALIDATION")
|
||||
IsDebug = GetEnvBool("GOPROXY_DEBUG")
|
||||
ProxyHTTPAddr = GetEnv("GOPROXY_HTTP_ADDR", ":80")
|
||||
ProxyHTTPSAddr = GetEnv("GOPROXY_HTTPS_ADDR", ":443")
|
||||
APIHTTPAddr = GetEnv("GOPROXY_API_ADDR", "127.0.0.1:8888")
|
||||
)
|
||||
|
||||
func getEnvBool(key string) bool {
|
||||
func GetEnvBool(key string) bool {
|
||||
return U.ParseBool(os.Getenv(key))
|
||||
}
|
||||
|
||||
func getEnv(key string, defaultValue string) string {
|
||||
func GetEnv(key string, defaultValue string) string {
|
||||
value, ok := os.LookupEnv(key)
|
||||
if !ok {
|
||||
value = defaultValue
|
||||
|
||||
74
src/common/ports.go
Normal file
74
src/common/ports.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package common
|
||||
|
||||
var (
|
||||
WellKnownHTTPPorts = map[string]bool{
|
||||
"80": true,
|
||||
"8000": true,
|
||||
"8008": true,
|
||||
"8080": true,
|
||||
"3000": true,
|
||||
}
|
||||
|
||||
ServiceNamePortMapTCP = map[string]int{
|
||||
"mssql": 1433,
|
||||
"mysql": 3306,
|
||||
"mariadb": 3306,
|
||||
"postgres": 5432,
|
||||
"rabbitmq": 5672,
|
||||
"redis": 6379,
|
||||
"memcached": 11211,
|
||||
"mongo": 27017,
|
||||
|
||||
"ssh": 22,
|
||||
"ftp": 21,
|
||||
"smtp": 25,
|
||||
"dns": 53,
|
||||
"pop3": 110,
|
||||
"imap": 143,
|
||||
}
|
||||
|
||||
ImageNamePortMap = func() (m map[string]int) {
|
||||
m = make(map[string]int, len(ServiceNamePortMapTCP)+len(imageNamePortMap))
|
||||
for k, v := range ServiceNamePortMapTCP {
|
||||
m[k] = v
|
||||
}
|
||||
for k, v := range imageNamePortMap {
|
||||
m[k] = v
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
imageNamePortMap = map[string]int{
|
||||
"adguardhome": 3000,
|
||||
"bazarr": 6767,
|
||||
"calibre-web": 8083,
|
||||
"changedetection.io": 3000,
|
||||
"dockge": 5001,
|
||||
"gitea": 3000,
|
||||
"gogs": 3000,
|
||||
"grafana": 3000,
|
||||
"home-assistant": 8123,
|
||||
"homebridge": 8581,
|
||||
"httpd": 80,
|
||||
"immich": 3001,
|
||||
"jellyfin": 8096,
|
||||
"lidarr": 8686,
|
||||
"minecraft-server": 25565,
|
||||
"nginx": 80,
|
||||
"nginx-proxy-manager": 81,
|
||||
"open-webui": 8080,
|
||||
"plex": 32400,
|
||||
"portainer": 9000,
|
||||
"portainer-ce": 9000,
|
||||
"prometheus": 9090,
|
||||
"prowlarr": 9696,
|
||||
"radarr": 7878,
|
||||
"radarr-sma": 7878,
|
||||
"rsshub": 1200,
|
||||
"rss-bridge": 80,
|
||||
"sonarr": 8989,
|
||||
"sonarr-sma": 8989,
|
||||
"uptime-kuma": 3001,
|
||||
"whisparr": 6969,
|
||||
}
|
||||
)
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
"github.com/yusing/go-proxy/watcher/events"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -94,7 +95,7 @@ func (cfg *Config) WatchChanges() {
|
||||
case <-cfg.watcherCtx.Done():
|
||||
return
|
||||
case event := <-eventCh:
|
||||
if event.Action.IsDelete() {
|
||||
if event.Action == events.ActionFileDeleted {
|
||||
cfg.stopProviders()
|
||||
} else {
|
||||
cfg.reloadReq <- struct{}{}
|
||||
@@ -107,71 +108,6 @@ func (cfg *Config) WatchChanges() {
|
||||
}()
|
||||
}
|
||||
|
||||
func (cfg *Config) FindRoute(alias string) R.Route {
|
||||
return F.MapFind(cfg.proxyProviders,
|
||||
func(p *PR.Provider) (R.Route, bool) {
|
||||
if route, ok := p.GetRoute(alias); ok {
|
||||
return route, true
|
||||
}
|
||||
return nil, false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject {
|
||||
routes := make(map[string]U.SerializedObject)
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
obj, err := U.Serialize(r)
|
||||
if err.HasError() {
|
||||
cfg.l.Error(err)
|
||||
return
|
||||
}
|
||||
obj["provider"] = p.GetName()
|
||||
obj["type"] = string(r.Type())
|
||||
routes[alias] = obj
|
||||
})
|
||||
return routes
|
||||
}
|
||||
|
||||
func (cfg *Config) Statistics() map[string]any {
|
||||
nTotalStreams := 0
|
||||
nTotalRPs := 0
|
||||
providerStats := make(map[string]any)
|
||||
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
s, ok := providerStats[p.GetName()]
|
||||
if !ok {
|
||||
s = make(map[string]int)
|
||||
}
|
||||
|
||||
stats := s.(map[string]int)
|
||||
switch r.Type() {
|
||||
case R.RouteTypeStream:
|
||||
stats["num_streams"]++
|
||||
nTotalStreams++
|
||||
case R.RouteTypeReverseProxy:
|
||||
stats["num_reverse_proxies"]++
|
||||
nTotalRPs++
|
||||
default:
|
||||
panic("bug: should not reach here")
|
||||
}
|
||||
})
|
||||
|
||||
return map[string]any{
|
||||
"num_total_streams": nTotalStreams,
|
||||
"num_total_reverse_proxies": nTotalRPs,
|
||||
"providers": providerStats,
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) DumpEntries() map[string]*M.RawEntry {
|
||||
entries := make(map[string]*M.RawEntry)
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
entries[alias] = r.Entry()
|
||||
})
|
||||
return entries
|
||||
}
|
||||
|
||||
func (cfg *Config) forEachRoute(do func(alias string, r R.Route, p *PR.Provider)) {
|
||||
cfg.proxyProviders.RangeAll(func(_ string, p *PR.Provider) {
|
||||
p.RangeRoutes(func(a string, r R.Route) {
|
||||
@@ -259,7 +195,7 @@ func (cfg *Config) loadProviders(providers *M.ProxyProviders) (res E.NestedError
|
||||
}
|
||||
|
||||
func (cfg *Config) controlProviders(action string, do func(*PR.Provider) E.NestedError) {
|
||||
errors := E.NewBuilder("cannot %s these providers", action)
|
||||
errors := E.NewBuilder("errors in %s these providers", action)
|
||||
|
||||
cfg.proxyProviders.RangeAll(func(name string, p *PR.Provider) {
|
||||
if err := do(p); err.HasError() {
|
||||
|
||||
82
src/config/query.go
Normal file
82
src/config/query.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
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"
|
||||
)
|
||||
|
||||
func (cfg *Config) DumpEntries() map[string]*M.RawEntry {
|
||||
entries := make(map[string]*M.RawEntry)
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
entries[alias] = r.Entry()
|
||||
})
|
||||
return entries
|
||||
}
|
||||
|
||||
func (cfg *Config) DumpProviders() map[string]*PR.Provider {
|
||||
entries := make(map[string]*PR.Provider)
|
||||
cfg.proxyProviders.RangeAll(func(name string, p *PR.Provider) {
|
||||
entries[name] = p
|
||||
})
|
||||
return entries
|
||||
}
|
||||
|
||||
func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject {
|
||||
routes := make(map[string]U.SerializedObject)
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
obj, err := U.Serialize(r)
|
||||
if err.HasError() {
|
||||
cfg.l.Error(err)
|
||||
return
|
||||
}
|
||||
obj["provider"] = p.GetName()
|
||||
obj["type"] = string(r.Type())
|
||||
routes[alias] = obj
|
||||
})
|
||||
return routes
|
||||
}
|
||||
|
||||
func (cfg *Config) Statistics() map[string]any {
|
||||
nTotalStreams := 0
|
||||
nTotalRPs := 0
|
||||
providerStats := make(map[string]any)
|
||||
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
s, ok := providerStats[p.GetName()]
|
||||
if !ok {
|
||||
s = make(map[string]int)
|
||||
}
|
||||
|
||||
stats := s.(map[string]int)
|
||||
switch r.Type() {
|
||||
case R.RouteTypeStream:
|
||||
stats["num_streams"]++
|
||||
nTotalStreams++
|
||||
case R.RouteTypeReverseProxy:
|
||||
stats["num_reverse_proxies"]++
|
||||
nTotalRPs++
|
||||
default:
|
||||
panic("bug: should not reach here")
|
||||
}
|
||||
})
|
||||
|
||||
return map[string]any{
|
||||
"num_total_streams": nTotalStreams,
|
||||
"num_total_reverse_proxies": nTotalRPs,
|
||||
"providers": providerStats,
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) FindRoute(alias string) R.Route {
|
||||
return F.MapFind(cfg.proxyProviders,
|
||||
func(p *PR.Provider) (R.Route, bool) {
|
||||
if route, ok := p.GetRoute(alias); ok {
|
||||
return route, true
|
||||
}
|
||||
return nil, false
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/common"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -48,9 +49,7 @@ func (c *Client) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
clientMapMu.Lock()
|
||||
defer clientMapMu.Unlock()
|
||||
delete(clientMap, c.key)
|
||||
clientMap.Delete(c.key)
|
||||
|
||||
client := c.Client
|
||||
c.Client = nil
|
||||
@@ -78,7 +77,7 @@ func ConnectClient(host string) (Client, E.NestedError) {
|
||||
defer clientMapMu.Unlock()
|
||||
|
||||
// check if client exists
|
||||
if client, ok := clientMap[host]; ok {
|
||||
if client, ok := clientMap.Load(host); ok {
|
||||
client.refCount.Add(1)
|
||||
return client, nil
|
||||
}
|
||||
@@ -129,23 +128,22 @@ func ConnectClient(host string) (Client, E.NestedError) {
|
||||
c.refCount.Add(1)
|
||||
c.l.Debugf("client connected")
|
||||
|
||||
clientMap[host] = c
|
||||
return clientMap[host], nil
|
||||
clientMap.Store(host, c)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func CloseAllClients() {
|
||||
clientMapMu.Lock()
|
||||
defer clientMapMu.Unlock()
|
||||
for _, client := range clientMap {
|
||||
client.Close()
|
||||
}
|
||||
clientMap = make(map[string]Client)
|
||||
clientMap.RangeAll(func(_ string, c Client) {
|
||||
c.Client.Close()
|
||||
})
|
||||
clientMap.Clear()
|
||||
logger.Debug("closed all clients")
|
||||
}
|
||||
|
||||
var (
|
||||
clientMap map[string]Client = make(map[string]Client)
|
||||
clientMapMu sync.Mutex
|
||||
clientMap F.Map[string, Client] = F.NewMapOf[string, Client]()
|
||||
clientMapMu sync.Mutex
|
||||
|
||||
clientOptEnvHost = []client.Opt{
|
||||
client.WithHostFromEnv(),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
|
||||
@@ -10,18 +10,21 @@ import (
|
||||
)
|
||||
|
||||
type ProxyProperties struct {
|
||||
DockerHost string `yaml:"-" json:"docker_host"`
|
||||
ContainerName string `yaml:"-" json:"container_name"`
|
||||
ImageName string `yaml:"-" json:"image_name"`
|
||||
Aliases []string `yaml:"-" json:"aliases"`
|
||||
IsExcluded bool `yaml:"-" json:"is_excluded"`
|
||||
FirstPort string `yaml:"-" json:"first_port"`
|
||||
IdleTimeout string `yaml:"-" json:"idle_timeout"`
|
||||
WakeTimeout string `yaml:"-" json:"wake_timeout"`
|
||||
StopMethod string `yaml:"-" json:"stop_method"`
|
||||
StopTimeout string `yaml:"-" json:"stop_timeout"` // stop_method = "stop" only
|
||||
StopSignal string `yaml:"-" json:"stop_signal"` // stop_method = "stop" | "kill" only
|
||||
Running bool `yaml:"-" json:"running"`
|
||||
DockerHost string `yaml:"-" json:"docker_host"`
|
||||
ContainerName string `yaml:"-" json:"container_name"`
|
||||
ImageName string `yaml:"-" json:"image_name"`
|
||||
PublicPortMapping PortMapping `yaml:"-" json:"public_port_mapping"` // non-zero publicPort:types.Port
|
||||
PrivatePortMapping PortMapping `yaml:"-" json:"private_port_mapping"` // privatePort:types.Port
|
||||
NetworkMode string `yaml:"-" json:"network_mode"`
|
||||
|
||||
Aliases []string `yaml:"-" json:"aliases"`
|
||||
IsExcluded bool `yaml:"-" json:"is_excluded"`
|
||||
IdleTimeout string `yaml:"-" json:"idle_timeout"`
|
||||
WakeTimeout string `yaml:"-" json:"wake_timeout"`
|
||||
StopMethod string `yaml:"-" json:"stop_method"`
|
||||
StopTimeout string `yaml:"-" json:"stop_timeout"` // stop_method = "stop" only
|
||||
StopSignal string `yaml:"-" json:"stop_signal"` // stop_method = "stop" | "kill" only
|
||||
Running bool `yaml:"-" json:"running"`
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
@@ -29,21 +32,25 @@ type Container struct {
|
||||
*ProxyProperties
|
||||
}
|
||||
|
||||
type PortMapping = map[string]types.Port
|
||||
|
||||
func FromDocker(c *types.Container, dockerHost string) (res Container) {
|
||||
res.Container = c
|
||||
res.ProxyProperties = &ProxyProperties{
|
||||
DockerHost: dockerHost,
|
||||
ContainerName: res.getName(),
|
||||
ImageName: res.getImageName(),
|
||||
Aliases: res.getAliases(),
|
||||
IsExcluded: U.ParseBool(res.getDeleteLabel(LableExclude)),
|
||||
FirstPort: res.firstPortOrEmpty(),
|
||||
IdleTimeout: res.getDeleteLabel(LabelIdleTimeout),
|
||||
WakeTimeout: res.getDeleteLabel(LabelWakeTimeout),
|
||||
StopMethod: res.getDeleteLabel(LabelStopMethod),
|
||||
StopTimeout: res.getDeleteLabel(LabelStopTimeout),
|
||||
StopSignal: res.getDeleteLabel(LabelStopSignal),
|
||||
Running: c.Status == "running" || c.State == "running",
|
||||
DockerHost: dockerHost,
|
||||
ContainerName: res.getName(),
|
||||
ImageName: res.getImageName(),
|
||||
PublicPortMapping: res.getPublicPortMapping(),
|
||||
PrivatePortMapping: res.getPrivatePortMapping(),
|
||||
NetworkMode: c.HostConfig.NetworkMode,
|
||||
Aliases: res.getAliases(),
|
||||
IsExcluded: U.ParseBool(res.getDeleteLabel(LabelExclude)),
|
||||
IdleTimeout: res.getDeleteLabel(LabelIdleTimeout),
|
||||
WakeTimeout: res.getDeleteLabel(LabelWakeTimeout),
|
||||
StopMethod: res.getDeleteLabel(LabelStopMethod),
|
||||
StopTimeout: res.getDeleteLabel(LabelStopTimeout),
|
||||
StopSignal: res.getDeleteLabel(LabelStopSignal),
|
||||
Running: c.Status == "running" || c.State == "running",
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -81,7 +88,7 @@ func (c Container) getDeleteLabel(label string) string {
|
||||
}
|
||||
|
||||
func (c Container) getAliases() []string {
|
||||
if l := c.getDeleteLabel(LableAliases); l != "" {
|
||||
if l := c.getDeleteLabel(LabelAliases); l != "" {
|
||||
return U.CommaSeperatedList(l)
|
||||
} else {
|
||||
return []string{c.getName()}
|
||||
@@ -98,14 +105,24 @@ func (c Container) getImageName() string {
|
||||
return slashSep[len(slashSep)-1]
|
||||
}
|
||||
|
||||
func (c Container) firstPortOrEmpty() string {
|
||||
if len(c.Ports) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, p := range c.Ports {
|
||||
if p.PublicPort != 0 {
|
||||
return fmt.Sprint(p.PublicPort)
|
||||
func (c Container) getPublicPortMapping() PortMapping {
|
||||
res := make(PortMapping)
|
||||
for _, v := range c.Ports {
|
||||
if v.PublicPort == 0 {
|
||||
continue
|
||||
}
|
||||
res[fmt.Sprint(v.PublicPort)] = v
|
||||
}
|
||||
return ""
|
||||
return res
|
||||
}
|
||||
|
||||
func (c Container) getPrivatePortMapping() PortMapping {
|
||||
res := make(PortMapping)
|
||||
for _, v := range c.Ports {
|
||||
if v.PublicPort == 0 {
|
||||
continue
|
||||
}
|
||||
res[fmt.Sprint(v.PrivatePort)] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
87
src/docker/idlewatcher/html/loading_page.html
Normal file
87
src/docker/idlewatcher/html/loading_page.html
Normal file
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
/* Global Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family: Inter, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #fff;
|
||||
background-color: #212121;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Spinner Styles */
|
||||
.spinner {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 16px solid #333;
|
||||
border-radius: 50%;
|
||||
border-top: 16px solid #66d9ef;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Error Styles */
|
||||
.error {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.error::before {
|
||||
content: "\26A0"; /* Unicode for warning symbol */
|
||||
font-size: 40px;
|
||||
color: #ff9900;
|
||||
}
|
||||
|
||||
/* Message Styles */
|
||||
.message {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
padding-left: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.onload = async function () {
|
||||
let result = await fetch(window.location.href, {
|
||||
headers: {
|
||||
{{ range $key, $value := .RequestHeaders }}
|
||||
'{{ $key }}' : {{ $value }}
|
||||
{{ end }}
|
||||
},
|
||||
}).then((resp) => resp.text())
|
||||
.catch((err) => {
|
||||
document.getElementById("message").innerText = err;
|
||||
});
|
||||
if (result) {
|
||||
document.documentElement.innerHTML = result
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<div class="{{.SpinnerClass}}"></div>
|
||||
<div class="message">{{.Message}}</div>
|
||||
</body>
|
||||
</html>
|
||||
93
src/docker/idlewatcher/http.go
Normal file
93
src/docker/idlewatcher/http.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type templateData struct {
|
||||
Title string
|
||||
Message string
|
||||
RequestHeaders http.Header
|
||||
SpinnerClass string
|
||||
}
|
||||
|
||||
//go:embed html/loading_page.html
|
||||
var loadingPage []byte
|
||||
var loadingPageTmpl = func() *template.Template {
|
||||
tmpl, err := template.New("loading").Parse(string(loadingPage))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tmpl
|
||||
}()
|
||||
|
||||
const (
|
||||
htmlContentType = "text/html; charset=utf-8"
|
||||
|
||||
errPrefix = "\u1000"
|
||||
|
||||
headerGoProxyTargetURL = "X-GoProxy-Target"
|
||||
headerContentType = "Content-Type"
|
||||
|
||||
spinnerClassSpinner = "spinner"
|
||||
spinnerClassErrorSign = "error"
|
||||
)
|
||||
|
||||
func (w *watcher) makeSuccResp(redirectURL string, resp *http.Response) (*http.Response, error) {
|
||||
h := make(http.Header)
|
||||
h.Set("Location", redirectURL)
|
||||
h.Set("Content-Length", "0")
|
||||
h.Set(headerContentType, htmlContentType)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusTemporaryRedirect,
|
||||
Header: h,
|
||||
Body: http.NoBody,
|
||||
TLS: resp.TLS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *watcher) makeErrResp(errFmt string, args ...any) (*http.Response, error) {
|
||||
return w.makeResp(errPrefix+errFmt, args...)
|
||||
}
|
||||
|
||||
func (w *watcher) makeResp(format string, args ...any) (*http.Response, error) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
|
||||
data := new(templateData)
|
||||
data.Title = w.ContainerName
|
||||
data.Message = strings.ReplaceAll(msg, "\n", "<br>")
|
||||
data.Message = strings.ReplaceAll(data.Message, " ", " ")
|
||||
data.RequestHeaders = make(http.Header)
|
||||
data.RequestHeaders.Add(headerGoProxyTargetURL, "window.location.href")
|
||||
if strings.HasPrefix(data.Message, errPrefix) {
|
||||
data.Message = strings.TrimLeft(data.Message, errPrefix)
|
||||
data.SpinnerClass = spinnerClassErrorSign
|
||||
} else {
|
||||
data.SpinnerClass = spinnerClassSpinner
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(make([]byte, 128)) // more than enough
|
||||
err := loadingPageTmpl.Execute(buf, data)
|
||||
if err != nil { // should never happen
|
||||
panic(err)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
Header: http.Header{
|
||||
headerContentType: {htmlContentType},
|
||||
"Cache-Control": {
|
||||
"no-cache",
|
||||
"no-store",
|
||||
"must-revalidate",
|
||||
},
|
||||
},
|
||||
Body: io.NopCloser(buf),
|
||||
ContentLength: int64(buf.Len()),
|
||||
}, nil
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package idlewatcher
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
roundTripper struct {
|
||||
@@ -12,3 +16,63 @@ type (
|
||||
func (rt roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return rt.patched(req)
|
||||
}
|
||||
|
||||
func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*http.Response, error) {
|
||||
// target site is ready, passthrough
|
||||
if w.ready.Load() {
|
||||
return origRoundTrip(req)
|
||||
}
|
||||
|
||||
// wake the container
|
||||
w.wakeCh <- struct{}{}
|
||||
|
||||
// initial request
|
||||
targetUrl := req.Header.Get(headerGoProxyTargetURL)
|
||||
if targetUrl == "" {
|
||||
return w.makeResp(
|
||||
"%s is starting... Please wait",
|
||||
w.ContainerName,
|
||||
)
|
||||
}
|
||||
|
||||
w.l.Debug("serving event")
|
||||
|
||||
// stream request
|
||||
rtDone := make(chan *http.Response, 1)
|
||||
ctx, cancel := context.WithTimeout(req.Context(), w.WakeTimeout)
|
||||
defer cancel()
|
||||
|
||||
// loop original round trip until success in a goroutine
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
default:
|
||||
resp, err := origRoundTrip(req)
|
||||
if err == nil {
|
||||
w.ready.Store(true)
|
||||
rtDone <- resp
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case resp := <-rtDone:
|
||||
return w.makeSuccResp(targetUrl, resp)
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return w.makeErrResp("Timed out waiting for %s to fully wake", w.ContainerName)
|
||||
}
|
||||
return w.makeErrResp("idlewatcher has stopped\n%s", w.ctx.Err().Error())
|
||||
case <-w.ctx.Done():
|
||||
return w.makeErrResp("idlewatcher has stopped\n%s", w.ctx.Err().Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -16,33 +14,45 @@ import (
|
||||
P "github.com/yusing/go-proxy/proxy"
|
||||
PT "github.com/yusing/go-proxy/proxy/fields"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
event "github.com/yusing/go-proxy/watcher/events"
|
||||
)
|
||||
|
||||
type watcher struct {
|
||||
*P.ReverseProxyEntry
|
||||
|
||||
client D.Client
|
||||
|
||||
refCount atomic.Int32
|
||||
|
||||
stopByMethod StopCallback
|
||||
wakeCh chan struct{}
|
||||
wakeDone chan E.NestedError
|
||||
running atomic.Bool
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
l logrus.FieldLogger
|
||||
}
|
||||
|
||||
type (
|
||||
watcher struct {
|
||||
*P.ReverseProxyEntry
|
||||
|
||||
client D.Client
|
||||
|
||||
ready atomic.Bool // whether the site is ready to accept connection
|
||||
stopByMethod StopCallback // send a docker command w.r.t. `stop_method`
|
||||
|
||||
wakeCh chan struct{}
|
||||
wakeDone chan E.NestedError
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
refCount *sync.WaitGroup
|
||||
|
||||
l logrus.FieldLogger
|
||||
}
|
||||
|
||||
WakeDone <-chan error
|
||||
WakeFunc func() WakeDone
|
||||
StopCallback func() E.NestedError
|
||||
)
|
||||
|
||||
var (
|
||||
mainLoopCtx context.Context
|
||||
mainLoopCancel context.CancelFunc
|
||||
mainLoopWg sync.WaitGroup
|
||||
|
||||
watcherMap = make(map[string]*watcher)
|
||||
watcherMapMu sync.Mutex
|
||||
|
||||
newWatcherCh = make(chan *watcher)
|
||||
|
||||
logger = logrus.WithField("module", "idle_watcher")
|
||||
)
|
||||
|
||||
func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||
failure := E.Failure("idle_watcher register")
|
||||
|
||||
@@ -67,12 +77,12 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||
w := &watcher{
|
||||
ReverseProxyEntry: entry,
|
||||
client: client,
|
||||
refCount: &sync.WaitGroup{},
|
||||
wakeCh: make(chan struct{}, 1),
|
||||
wakeDone: make(chan E.NestedError, 1),
|
||||
l: logger.WithField("container", entry.ContainerName),
|
||||
}
|
||||
w.refCount.Add(1)
|
||||
w.running.Store(entry.ContainerRunning)
|
||||
w.stopByMethod = w.getStopCallback()
|
||||
|
||||
watcherMap[w.ContainerName] = w
|
||||
@@ -84,20 +94,9 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// If the container is not registered, this is no-op
|
||||
func Unregister(containerName string) {
|
||||
watcherMapMu.Lock()
|
||||
defer watcherMapMu.Unlock()
|
||||
|
||||
if w, ok := watcherMap[containerName]; ok {
|
||||
if w.refCount.Add(-1) > 0 {
|
||||
return
|
||||
}
|
||||
if w.cancel != nil {
|
||||
w.cancel()
|
||||
}
|
||||
w.client.Close()
|
||||
delete(watcherMap, containerName)
|
||||
w.refCount.Add(-1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,8 +106,6 @@ func Start() {
|
||||
|
||||
mainLoopCtx, mainLoopCancel = context.WithCancel(context.Background())
|
||||
|
||||
defer mainLoopWg.Wait()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-mainLoopCtx.Done():
|
||||
@@ -117,8 +114,11 @@ func Start() {
|
||||
w.l.Debug("registered")
|
||||
mainLoopWg.Add(1)
|
||||
go func() {
|
||||
w.watch()
|
||||
Unregister(w.ContainerName)
|
||||
w.watchUntilCancel()
|
||||
w.refCount.Wait() // wait for 0 ref count
|
||||
|
||||
w.client.Close()
|
||||
delete(watcherMap, w.ContainerName)
|
||||
w.l.Debug("unregistered")
|
||||
mainLoopWg.Done()
|
||||
}()
|
||||
@@ -137,31 +137,6 @@ func (w *watcher) PatchRoundTripper(rtp http.RoundTripper) roundTripper {
|
||||
}}
|
||||
}
|
||||
|
||||
func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*http.Response, error) {
|
||||
w.wakeCh <- struct{}{}
|
||||
|
||||
if w.running.Load() {
|
||||
return origRoundTrip(req)
|
||||
}
|
||||
timeout := time.After(w.WakeTimeout)
|
||||
|
||||
for {
|
||||
if w.running.Load() {
|
||||
return origRoundTrip(req)
|
||||
}
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
return nil, req.Context().Err()
|
||||
case err := <-w.wakeDone:
|
||||
if err != nil {
|
||||
return nil, err.Error()
|
||||
}
|
||||
case <-timeout:
|
||||
return getLoadingResponse(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) containerStop() error {
|
||||
return w.client.ContainerStop(w.ctx, w.ContainerName, container.StopOptions{
|
||||
Signal: string(w.StopSignal),
|
||||
@@ -205,7 +180,6 @@ func (w *watcher) wakeIfStopped() E.NestedError {
|
||||
case "paused":
|
||||
return E.From(w.containerUnpause())
|
||||
case "running":
|
||||
w.running.Store(true)
|
||||
return nil
|
||||
default:
|
||||
return E.Unexpected("container state", status)
|
||||
@@ -236,15 +210,12 @@ func (w *watcher) getStopCallback() StopCallback {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) watch() {
|
||||
watcherCtx, watcherCancel := context.WithCancel(context.Background())
|
||||
w.ctx = watcherCtx
|
||||
w.cancel = watcherCancel
|
||||
|
||||
dockerWatcher := W.NewDockerWatcherWithClient(w.client)
|
||||
|
||||
func (w *watcher) watchUntilCancel() {
|
||||
defer close(w.wakeCh)
|
||||
|
||||
w.ctx, w.cancel = context.WithCancel(context.Background())
|
||||
|
||||
dockerWatcher := W.NewDockerWatcherWithClient(w.client)
|
||||
dockerEventCh, dockerEventErrCh := dockerWatcher.EventsWithOptions(w.ctx, W.DockerListOptions{
|
||||
Filters: W.NewDockerFilter(
|
||||
W.DockerFilterContainer,
|
||||
@@ -265,7 +236,7 @@ func (w *watcher) watch() {
|
||||
select {
|
||||
case <-mainLoopCtx.Done():
|
||||
w.cancel()
|
||||
case <-watcherCtx.Done():
|
||||
case <-w.ctx.Done():
|
||||
w.l.Debug("stopped")
|
||||
return
|
||||
case err := <-dockerEventErrCh:
|
||||
@@ -273,16 +244,18 @@ func (w *watcher) watch() {
|
||||
w.l.Error(E.FailWith("docker watcher", err))
|
||||
}
|
||||
case e := <-dockerEventCh:
|
||||
switch e.Action {
|
||||
case event.ActionDockerStartUnpause:
|
||||
w.running.Store(true)
|
||||
w.l.Infof("%s %s", e.ActorName, e.Action)
|
||||
case event.ActionDockerStopPause:
|
||||
w.running.Store(false)
|
||||
w.l.Infof("%s %s", e.ActorName, e.Action)
|
||||
switch {
|
||||
// create / start / unpause
|
||||
case e.Action.IsContainerWake():
|
||||
ticker.Reset(w.IdleTimeout)
|
||||
w.l.Info(e)
|
||||
default: // stop / pause / kill
|
||||
ticker.Stop()
|
||||
w.ready.Store(false)
|
||||
w.l.Info(e)
|
||||
}
|
||||
case <-ticker.C:
|
||||
w.l.Debug("timeout")
|
||||
w.l.Debug("idle timeout")
|
||||
ticker.Stop()
|
||||
if err := w.stopByMethod(); err != nil && err.IsNot(context.Canceled) {
|
||||
w.l.Error(E.FailWith("stop", err).Extraf("stop method: %s", w.StopMethod))
|
||||
@@ -301,57 +274,3 @@ func (w *watcher) watch() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getLoadingResponse() *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
Header: http.Header{
|
||||
"Content-Type": {"text/html"},
|
||||
"Cache-Control": {
|
||||
"no-cache",
|
||||
"no-store",
|
||||
"must-revalidate",
|
||||
},
|
||||
},
|
||||
Body: io.NopCloser(bytes.NewReader((loadingPage))),
|
||||
ContentLength: int64(len(loadingPage)),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
mainLoopCtx context.Context
|
||||
mainLoopCancel context.CancelFunc
|
||||
mainLoopWg sync.WaitGroup
|
||||
|
||||
watcherMap = make(map[string]*watcher)
|
||||
watcherMapMu sync.Mutex
|
||||
|
||||
newWatcherCh = make(chan *watcher)
|
||||
|
||||
logger = logrus.WithField("module", "idle_watcher")
|
||||
|
||||
loadingPage = []byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Loading...</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
setTimeout(function() {
|
||||
window.location.reload()
|
||||
}, 1000)
|
||||
// fetch(window.location.href)
|
||||
// .then(resp => resp.text())
|
||||
// .then(data => { document.body.innerHTML = data; })
|
||||
// .catch(err => { document.body.innerHTML = 'Error: ' + err; });
|
||||
};
|
||||
</script>
|
||||
<h1>Container is starting... Please wait</h1>
|
||||
</body>
|
||||
</html>
|
||||
`[1:])
|
||||
)
|
||||
|
||||
@@ -3,8 +3,8 @@ package docker
|
||||
const (
|
||||
WildcardAlias = "*"
|
||||
|
||||
LableAliases = NSProxy + ".aliases"
|
||||
LableExclude = NSProxy + ".exclude"
|
||||
LabelAliases = NSProxy + ".aliases"
|
||||
LabelExclude = NSProxy + ".exclude"
|
||||
LabelIdleTimeout = NSProxy + ".idle_timeout"
|
||||
LabelWakeTimeout = NSProxy + ".wake_timeout"
|
||||
LabelStopMethod = NSProxy + ".stop_method"
|
||||
|
||||
@@ -25,6 +25,7 @@ func NewBuilder(format string, args ...any) Builder {
|
||||
func (b Builder) Add(err NestedError) Builder {
|
||||
if err != nil {
|
||||
b.Lock()
|
||||
// TODO: if err severity is higher than b.severity, update b.severity
|
||||
b.errors = append(b.errors, err)
|
||||
b.Unlock()
|
||||
}
|
||||
@@ -54,15 +55,19 @@ func (b Builder) WithSeverity(s Severity) Builder {
|
||||
func (b Builder) Build() NestedError {
|
||||
if len(b.errors) == 0 {
|
||||
return nil
|
||||
} else if len(b.errors) == 1 {
|
||||
return b.errors[0]
|
||||
}
|
||||
return Join(b.message, b.errors...).Severity(b.severity)
|
||||
}
|
||||
|
||||
func (b Builder) To(ptr *NestedError) {
|
||||
if *ptr == nil {
|
||||
if ptr == nil {
|
||||
return
|
||||
} else if *ptr == nil {
|
||||
*ptr = b.Build()
|
||||
} else {
|
||||
**ptr = *b.Build()
|
||||
(*ptr).With(b.Build())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,10 +43,10 @@ func TestBuilderNested(t *testing.T) {
|
||||
expected2 :=
|
||||
(`error occurred:
|
||||
- Action 1 failed:
|
||||
- invalid Inner: 2
|
||||
- invalid Inner: 1
|
||||
- invalid Inner: "1"
|
||||
- invalid Inner: "2"
|
||||
- Action 2 failed:
|
||||
- invalid Inner: 3`)
|
||||
- invalid Inner: "3"`)
|
||||
if got != expected1 && got != expected2 {
|
||||
t.Errorf("expected \n%s, got \n%s", expected1, got)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ type (
|
||||
)
|
||||
|
||||
const (
|
||||
SeverityFatal Severity = iota
|
||||
SeverityWarning
|
||||
SeverityWarning Severity = iota
|
||||
SeverityFatal
|
||||
)
|
||||
|
||||
func From(err error) NestedError {
|
||||
@@ -118,9 +118,9 @@ func (ne NestedError) With(s any) NestedError {
|
||||
case string:
|
||||
msg = ss
|
||||
case fmt.Stringer:
|
||||
msg = ss.String()
|
||||
return ne.append(ss.String())
|
||||
default:
|
||||
msg = fmt.Sprint(s)
|
||||
return ne.append(fmt.Sprint(s))
|
||||
}
|
||||
return ne.withError(From(errors.New(msg)))
|
||||
}
|
||||
@@ -201,6 +201,14 @@ func (ne NestedError) withError(err NestedError) NestedError {
|
||||
return ne
|
||||
}
|
||||
|
||||
func (ne NestedError) append(msg string) NestedError {
|
||||
if ne == nil {
|
||||
return nil
|
||||
}
|
||||
ne.err = fmt.Errorf("%w %s", ne.err, msg)
|
||||
return ne
|
||||
}
|
||||
|
||||
func (ne NestedError) writeToSB(sb *strings.Builder, level int, prefix string) {
|
||||
for i := 0; i < level; i++ {
|
||||
sb.WriteString(" ")
|
||||
|
||||
@@ -10,17 +10,19 @@ var (
|
||||
ErrUnsupported = stderrors.New("unsupported")
|
||||
ErrUnexpected = stderrors.New("unexpected")
|
||||
ErrNotExists = stderrors.New("does not exist")
|
||||
ErrMissing = stderrors.New("missing")
|
||||
ErrAlreadyExist = stderrors.New("already exist")
|
||||
ErrOutOfRange = stderrors.New("out of range")
|
||||
)
|
||||
|
||||
const fmtSubjectWhat = "%w %v: %v"
|
||||
const fmtSubjectWhat = "%w %v: %q"
|
||||
|
||||
func Failure(what string) NestedError {
|
||||
return errorf("%s %w", what, ErrFailure)
|
||||
}
|
||||
|
||||
func FailedWhy(what string, why string) NestedError {
|
||||
return errorf("%s %w because %s", what, ErrFailure, why)
|
||||
return Failure(what).With(why)
|
||||
}
|
||||
|
||||
func FailWith(what string, err any) NestedError {
|
||||
@@ -47,6 +49,14 @@ func NotExist(subject, what any) NestedError {
|
||||
return errorf("%v %w: %v", subject, ErrNotExists, what)
|
||||
}
|
||||
|
||||
func Missing(subject any) NestedError {
|
||||
return errorf("%w %v", ErrMissing, subject)
|
||||
}
|
||||
|
||||
func AlreadyExist(subject, what any) NestedError {
|
||||
return errorf("%v %w: %v", subject, ErrAlreadyExist, what)
|
||||
}
|
||||
|
||||
func OutOfRange(subject string, value any) NestedError {
|
||||
return errorf("%v %w: %v", subject, ErrOutOfRange, value)
|
||||
}
|
||||
|
||||
66
src/main.go
66
src/main.go
@@ -8,6 +8,9 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -27,7 +30,14 @@ import (
|
||||
|
||||
func main() {
|
||||
args := common.GetArgs()
|
||||
|
||||
if args.Command == common.CommandSetup {
|
||||
Setup()
|
||||
return
|
||||
}
|
||||
|
||||
l := logrus.WithField("module", "main")
|
||||
onShutdown := F.NewSlice[func()]()
|
||||
|
||||
if common.IsDebug {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
@@ -47,13 +57,12 @@ func main() {
|
||||
|
||||
if args.Command == common.CommandReload {
|
||||
if err := apiUtils.ReloadServer(); err.HasError() {
|
||||
l.Fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Print("ok")
|
||||
return
|
||||
}
|
||||
|
||||
onShutdown := F.NewSlice[func()]()
|
||||
|
||||
// exit if only validate config
|
||||
if args.Command == common.CommandValidate {
|
||||
data, err := os.ReadFile(common.ConfigPath)
|
||||
@@ -72,19 +81,19 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if args.Command == common.CommandListConfigs {
|
||||
switch args.Command {
|
||||
case common.CommandListConfigs:
|
||||
printJSON(cfg.Value())
|
||||
return
|
||||
}
|
||||
|
||||
if args.Command == common.CommandListRoutes {
|
||||
case common.CommandListRoutes:
|
||||
printJSON(cfg.RoutesByAlias())
|
||||
return
|
||||
}
|
||||
|
||||
if args.Command == common.CommandDebugListEntries {
|
||||
case common.CommandDebugListEntries:
|
||||
printJSON(cfg.DumpEntries())
|
||||
return
|
||||
case common.CommandDebugListProviders:
|
||||
printJSON(cfg.DumpProviders())
|
||||
return
|
||||
}
|
||||
|
||||
cfg.StartProxyProviders()
|
||||
@@ -106,25 +115,14 @@ func main() {
|
||||
autocert := cfg.GetAutoCertProvider()
|
||||
|
||||
if autocert != nil {
|
||||
if err = autocert.LoadCert(); err.HasError() {
|
||||
if !err.Is(os.ErrNotExist) { // ignore if cert doesn't exist
|
||||
l.Error(err)
|
||||
}
|
||||
l.Debug("obtaining cert due to error loading cert")
|
||||
if err = autocert.ObtainCert(); err.HasError() {
|
||||
l.Warn(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err.NoError() {
|
||||
ctx, certRenewalCancel := context.WithCancel(context.Background())
|
||||
go autocert.ScheduleRenewal(ctx)
|
||||
onShutdown.Add(certRenewalCancel)
|
||||
}
|
||||
|
||||
for _, expiry := range autocert.GetExpiries() {
|
||||
l.Infof("certificate expire on %s", expiry)
|
||||
break
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
if err = autocert.Setup(ctx); err != nil && err.IsWarning() {
|
||||
cancel()
|
||||
l.Warn(err)
|
||||
} else if err.IsFatal() {
|
||||
l.Fatal(err)
|
||||
} else {
|
||||
onShutdown.Add(cancel)
|
||||
}
|
||||
} else {
|
||||
l.Info("autocert not configured")
|
||||
@@ -165,7 +163,9 @@ func main() {
|
||||
wg.Add(onShutdown.Size())
|
||||
onShutdown.ForEach(func(f func()) {
|
||||
go func() {
|
||||
l.Debugf("waiting for %s to complete...", funcName(f))
|
||||
f()
|
||||
l.Debugf("%s done", funcName(f))
|
||||
wg.Done()
|
||||
}()
|
||||
})
|
||||
@@ -180,9 +180,17 @@ func main() {
|
||||
logrus.Info("shutdown complete")
|
||||
case <-timeout:
|
||||
logrus.Info("timeout waiting for shutdown")
|
||||
onShutdown.ForEach(func(f func()) {
|
||||
l.Warnf("%s() is still running", funcName(f))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func funcName(f func()) string {
|
||||
parts := strings.Split(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name(), "/go-proxy/")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
func printJSON(obj any) {
|
||||
j, err := E.Check(json.Marshal(obj))
|
||||
if err.HasError() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -31,50 +32,72 @@ type (
|
||||
|
||||
var NewProxyEntries = F.NewMapOf[string, *RawEntry]
|
||||
|
||||
func (e *RawEntry) SetDefaults() {
|
||||
if e.ProxyProperties == nil {
|
||||
func (e *RawEntry) FillMissingFields() bool {
|
||||
isDocker := e.ProxyProperties != nil
|
||||
|
||||
if !isDocker {
|
||||
e.ProxyProperties = &D.ProxyProperties{}
|
||||
}
|
||||
|
||||
if e.Scheme == "" {
|
||||
switch {
|
||||
case strings.ContainsRune(e.Port, ':'):
|
||||
e.Scheme = "tcp"
|
||||
case e.ProxyProperties != nil:
|
||||
if _, ok := ServiceNamePortMapTCP[e.ImageName]; ok {
|
||||
e.Scheme = "tcp"
|
||||
if e.Port == "" {
|
||||
if port, ok := ServiceNamePortMapTCP[e.ImageName]; ok {
|
||||
e.Port = strconv.Itoa(port)
|
||||
} else if port, ok := ImageNamePortMap[e.ImageName]; ok {
|
||||
e.Port = strconv.Itoa(port)
|
||||
} else {
|
||||
switch {
|
||||
case e.Scheme == "https":
|
||||
e.Port = "443"
|
||||
case !isDocker:
|
||||
e.Port = "80"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if e.PublicPortMapping != nil && e.NetworkMode != "host" {
|
||||
if _, ok := e.PublicPortMapping[e.Port]; !ok { // port is not exposed, but specified
|
||||
// try to fallback to first public port
|
||||
if len(e.PublicPortMapping) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, p := range e.PublicPortMapping {
|
||||
e.Port = fmt.Sprint(p.PublicPort)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if e.Scheme == "" {
|
||||
switch e.Port {
|
||||
case "443", "8443":
|
||||
if _, ok := ServiceNamePortMapTCP[e.ImageName]; ok {
|
||||
e.Scheme = "tcp"
|
||||
} else if strings.ContainsRune(e.Port, ':') {
|
||||
e.Scheme = "tcp"
|
||||
} else if _, ok := WellKnownHTTPPorts[e.Port]; ok {
|
||||
e.Scheme = "http"
|
||||
} else if e.Port == "443" {
|
||||
e.Scheme = "https"
|
||||
default:
|
||||
} else if isDocker {
|
||||
if e.Port == "" {
|
||||
return false
|
||||
}
|
||||
if p, ok := e.PublicPortMapping[e.Port]; ok {
|
||||
if p.Type == "udp" {
|
||||
e.Scheme = "udp"
|
||||
} else {
|
||||
e.Scheme = "http"
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
e.Scheme = "http"
|
||||
}
|
||||
}
|
||||
|
||||
if e.Host == "" {
|
||||
e.Host = "localhost"
|
||||
}
|
||||
if e.Port == "" {
|
||||
e.Port = e.FirstPort
|
||||
}
|
||||
if e.Port == "" {
|
||||
if port, ok := ServiceNamePortMapTCP[e.Port]; ok {
|
||||
e.Port = strconv.Itoa(port)
|
||||
} else if port, ok := ImageNamePortMapHTTP[e.Port]; ok {
|
||||
e.Port = strconv.Itoa(port)
|
||||
} else {
|
||||
switch e.Scheme {
|
||||
case "http":
|
||||
e.Port = "80"
|
||||
case "https":
|
||||
e.Port = "443"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if e.IdleTimeout == "" {
|
||||
e.IdleTimeout = IdleTimeoutDefault
|
||||
}
|
||||
@@ -87,4 +110,6 @@ func (e *RawEntry) SetDefaults() {
|
||||
if e.StopMethod == "" {
|
||||
e.StopMethod = StopMethodDefault
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package proxy
|
||||
|
||||
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...)
|
||||
)
|
||||
@@ -44,7 +44,10 @@ func (rp *ReverseProxyEntry) UseIdleWatcher() bool {
|
||||
}
|
||||
|
||||
func ValidateEntry(m *M.RawEntry) (any, E.NestedError) {
|
||||
m.SetDefaults()
|
||||
if !m.FillMissingFields() {
|
||||
return nil, E.Missing("fields")
|
||||
}
|
||||
|
||||
scheme, err := T.NewScheme(m.Scheme)
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
|
||||
@@ -13,7 +13,7 @@ func NewPathPattern(s string) (PathPattern, E.NestedError) {
|
||||
if len(s) == 0 {
|
||||
return "", E.Invalid("path", "must not be empty")
|
||||
}
|
||||
if !pathPattern.MatchString(string(s)) {
|
||||
if !pathPattern.MatchString(s) {
|
||||
return "", E.Invalid("path pattern", s)
|
||||
}
|
||||
return PathPattern(s), nil
|
||||
@@ -34,4 +34,4 @@ func ValidatePathPatterns(s []string) (PathPatterns, E.NestedError) {
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
var pathPattern = regexp.MustCompile("^((GET|POST|DELETE|PUT|PATCH|HEAD|OPTIONS|CONNECT)\\s)?(/\\w*)+/?$")
|
||||
var pathPattern = regexp.MustCompile(`^(/[-\w./]*({\$\})?|((GET|POST|DELETE|PUT|HEAD|OPTION) /[-\w./]*({\$\})?))$`)
|
||||
|
||||
47
src/proxy/fields/path_pattern_test.go
Normal file
47
src/proxy/fields/path_pattern_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
U "github.com/yusing/go-proxy/utils/testing"
|
||||
)
|
||||
|
||||
var validPatterns = []string{
|
||||
"/",
|
||||
"/index.html",
|
||||
"/somepage/",
|
||||
"/drive/abc.mp4",
|
||||
"/{$}",
|
||||
"/some-page/{$}",
|
||||
"GET /",
|
||||
"GET /static/{$}",
|
||||
"GET /drive/abc.mp4",
|
||||
"GET /drive/abc.mp4/{$}",
|
||||
"POST /auth",
|
||||
"DELETE /user/",
|
||||
"PUT /storage/id/",
|
||||
}
|
||||
|
||||
var invalidPatterns = []string{
|
||||
"/$",
|
||||
"/{$}{$}",
|
||||
"/{$}/{$}",
|
||||
"/index.html$",
|
||||
"get /",
|
||||
"GET/",
|
||||
"GET /$",
|
||||
"GET /drive/{$}/abc.mp4/",
|
||||
"OPTION /config/{$}/abc.conf/{$}",
|
||||
}
|
||||
|
||||
func TestPathPatternRegex(t *testing.T) {
|
||||
for _, pattern := range validPatterns {
|
||||
_, err := NewPathPattern(pattern)
|
||||
U.ExpectNoError(t, err.Error())
|
||||
}
|
||||
for _, pattern := range invalidPatterns {
|
||||
_, err := NewPathPattern(pattern)
|
||||
U.ExpectError2(t, pattern, E.ErrInvalid, err.Error())
|
||||
}
|
||||
}
|
||||
@@ -13,22 +13,23 @@ func ValidatePort(v string) (Port, E.NestedError) {
|
||||
if err != nil {
|
||||
return ErrPort, E.Invalid("port number", v).With(err)
|
||||
}
|
||||
return NewPortInt(p)
|
||||
return ValidatePortInt(p)
|
||||
}
|
||||
|
||||
func NewPortInt[Int int | uint16](v Int) (Port, E.NestedError) {
|
||||
pp := Port(v)
|
||||
if err := pp.boundCheck(); err.HasError() {
|
||||
return ErrPort, err
|
||||
func ValidatePortInt[Int int | uint16](v Int) (Port, E.NestedError) {
|
||||
p := Port(v)
|
||||
if !p.inBound() {
|
||||
return ErrPort, E.OutOfRange("port", p)
|
||||
}
|
||||
return pp, nil
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p Port) boundCheck() E.NestedError {
|
||||
if p < MinPort || p > MaxPort {
|
||||
return E.Invalid("port", p)
|
||||
}
|
||||
return nil
|
||||
func (p Port) inBound() bool {
|
||||
return p >= MinPort && p <= MaxPort
|
||||
}
|
||||
|
||||
func (p Port) String() string {
|
||||
return strconv.Itoa(int(p))
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/common"
|
||||
@@ -15,36 +14,42 @@ type StreamPort struct {
|
||||
|
||||
func ValidateStreamPort(p string) (StreamPort, E.NestedError) {
|
||||
split := strings.Split(p, ":")
|
||||
if len(split) != 2 {
|
||||
return StreamPort{}, E.Invalid("stream port", fmt.Sprintf("%q", p)).With("should be in 'x:y' format")
|
||||
|
||||
switch len(split) {
|
||||
case 1:
|
||||
split = []string{"0", split[0]}
|
||||
case 2:
|
||||
break
|
||||
default:
|
||||
return ErrStreamPort, E.Invalid("stream port", p).With("too many colons")
|
||||
}
|
||||
|
||||
listeningPort, err := ValidatePort(split[0])
|
||||
if err.HasError() {
|
||||
return StreamPort{}, err
|
||||
}
|
||||
if err = listeningPort.boundCheck(); err.HasError() {
|
||||
return StreamPort{}, err
|
||||
if err != nil {
|
||||
return ErrStreamPort, err
|
||||
}
|
||||
|
||||
proxyPort, err := ValidatePort(split[1])
|
||||
if err.HasError() {
|
||||
if err.Is(E.ErrOutOfRange) {
|
||||
return ErrStreamPort, err
|
||||
} else if proxyPort == 0 {
|
||||
return ErrStreamPort, E.Invalid("stream port", p).With("proxy port cannot be 0")
|
||||
} else if err != nil {
|
||||
proxyPort, err = parseNameToPort(split[1])
|
||||
if err.HasError() {
|
||||
return StreamPort{}, err
|
||||
if err != nil {
|
||||
return ErrStreamPort, E.Invalid("stream port", p).With(proxyPort)
|
||||
}
|
||||
}
|
||||
if err = proxyPort.boundCheck(); err.HasError() {
|
||||
return StreamPort{}, err
|
||||
}
|
||||
|
||||
return StreamPort{ListeningPort: listeningPort, ProxyPort: proxyPort}, nil
|
||||
return StreamPort{listeningPort, proxyPort}, nil
|
||||
}
|
||||
|
||||
func parseNameToPort(name string) (Port, E.NestedError) {
|
||||
port, ok := common.ServiceNamePortMapTCP[name]
|
||||
if !ok {
|
||||
return -1, E.Unsupported("service", name)
|
||||
return ErrPort, E.Invalid("service", name)
|
||||
}
|
||||
return Port(port), nil
|
||||
}
|
||||
|
||||
var ErrStreamPort = StreamPort{ErrPort, ErrPort}
|
||||
|
||||
48
src/proxy/fields/stream_port_test.go
Normal file
48
src/proxy/fields/stream_port_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
. "github.com/yusing/go-proxy/utils/testing"
|
||||
)
|
||||
|
||||
var validPorts = []string{
|
||||
"1234:5678",
|
||||
"0:2345",
|
||||
"2345",
|
||||
"1234:postgres",
|
||||
}
|
||||
|
||||
var invalidPorts = []string{
|
||||
"",
|
||||
"123:",
|
||||
"0:",
|
||||
":1234",
|
||||
"1234:1234:1234",
|
||||
"qwerty",
|
||||
"asdfgh:asdfgh",
|
||||
"1234:asdfgh",
|
||||
}
|
||||
|
||||
var outOfRangePorts = []string{
|
||||
"-1:1234",
|
||||
"1234:-1",
|
||||
"65536",
|
||||
"0:65536",
|
||||
}
|
||||
|
||||
func TestStreamPort(t *testing.T) {
|
||||
for _, port := range validPorts {
|
||||
_, err := ValidateStreamPort(port)
|
||||
ExpectNoError(t, err.Error())
|
||||
}
|
||||
for _, port := range invalidPorts {
|
||||
_, err := ValidateStreamPort(port)
|
||||
ExpectError2(t, port, E.ErrInvalid, err.Error())
|
||||
}
|
||||
for _, port := range outOfRangePorts {
|
||||
_, err := ValidateStreamPort(port)
|
||||
ExpectError2(t, port, E.ErrOutOfRange, err.Error())
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func ValidateStreamScheme(s string) (ss *StreamScheme, err E.NestedError) {
|
||||
}
|
||||
|
||||
func (s StreamScheme) String() string {
|
||||
return fmt.Sprintf("%s -> %s", s.ListeningScheme, s.ProxyScheme)
|
||||
return fmt.Sprintf("%s:%s", s.ListeningScheme, s.ProxyScheme)
|
||||
}
|
||||
|
||||
// IsCoherent checks if the ListeningScheme and ProxyScheme of the StreamScheme are equal.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -26,6 +27,10 @@ func DockerProviderImpl(dockerHost string) (ProviderImpl, E.NestedError) {
|
||||
return &DockerProvider{dockerHost: dockerHost, hostname: hostname}, nil
|
||||
}
|
||||
|
||||
func (p *DockerProvider) String() string {
|
||||
return fmt.Sprintf("docker:%s", p.dockerHost)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) NewWatcher() W.Watcher {
|
||||
return W.NewDockerWatcher(p.dockerHost)
|
||||
}
|
||||
@@ -133,25 +138,28 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.Ra
|
||||
}
|
||||
|
||||
// selecting correct host port
|
||||
if container.HostConfig.NetworkMode != "host" {
|
||||
for _, a := range container.Aliases {
|
||||
entry, ok := entries.Load(a)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, p := range container.Ports {
|
||||
containerPort := strconv.Itoa(int(p.PrivatePort))
|
||||
publicPort := strconv.Itoa(int(p.PublicPort))
|
||||
replacePrivPorts := func() {
|
||||
if container.HostConfig.NetworkMode != "host" {
|
||||
entries.RangeAll(func(_ string, entry *M.RawEntry) {
|
||||
entryPortSplit := strings.Split(entry.Port, ":")
|
||||
if len(entryPortSplit) == 2 && entryPortSplit[1] == containerPort {
|
||||
entryPortSplit[1] = publicPort
|
||||
} else if entryPortSplit[0] == containerPort {
|
||||
entryPortSplit[0] = publicPort
|
||||
n := len(entryPortSplit)
|
||||
// if the port matches the proxy port, replace it with the public port
|
||||
if p, ok := container.PrivatePortMapping[entryPortSplit[n-1]]; ok {
|
||||
entryPortSplit[n-1] = fmt.Sprint(p.PublicPort)
|
||||
entry.Port = strings.Join(entryPortSplit, ":")
|
||||
}
|
||||
entry.Port = strings.Join(entryPortSplit, ":")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
replacePrivPorts()
|
||||
|
||||
// remove all entries that failed to fill in missing fields
|
||||
entries.RemoveAll(func(re *M.RawEntry) bool {
|
||||
return !re.FillMissingFields()
|
||||
})
|
||||
|
||||
// do it again since the port may got filled in
|
||||
replacePrivPorts()
|
||||
|
||||
return entries, errors.Build().Subject(container.ContainerName)
|
||||
}
|
||||
@@ -183,7 +191,7 @@ func (p *DockerProvider) applyLabel(container D.Container, entries M.RawEntries,
|
||||
return ref
|
||||
}
|
||||
if index < 1 || index > len(container.Aliases) {
|
||||
refErr.Add(E.Invalid("index", ref).Extraf("index out of range"))
|
||||
refErr.Add(E.OutOfRange("index", ref))
|
||||
return ref
|
||||
}
|
||||
return container.Aliases[index-1]
|
||||
|
||||
@@ -8,15 +8,12 @@ import (
|
||||
"github.com/yusing/go-proxy/common"
|
||||
D "github.com/yusing/go-proxy/docker"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
P "github.com/yusing/go-proxy/proxy"
|
||||
T "github.com/yusing/go-proxy/proxy/fields"
|
||||
|
||||
. "github.com/yusing/go-proxy/utils/testing"
|
||||
)
|
||||
|
||||
func get[KT comparable, VT any](m F.Map[KT, VT], key KT) VT {
|
||||
v, _ := m.Load(key)
|
||||
return v
|
||||
}
|
||||
|
||||
var dummyNames = []string{"/a"}
|
||||
|
||||
func TestApplyLabelFieldValidity(t *testing.T) {
|
||||
@@ -48,10 +45,10 @@ X_Custom_Header2: value3
|
||||
"X-Custom-Header2",
|
||||
}
|
||||
var p DockerProvider
|
||||
var c = D.FromDocker(&types.Container{
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LableAliases: "a,b",
|
||||
D.LabelAliases: "a,b",
|
||||
D.LabelIdleTimeout: common.IdleTimeoutDefault,
|
||||
D.LabelStopMethod: common.StopMethodDefault,
|
||||
D.LabelStopSignal: "SIGTERM",
|
||||
@@ -65,11 +62,16 @@ X_Custom_Header2: value3
|
||||
"proxy.a.path_patterns": pathPatterns,
|
||||
"proxy.a.set_headers": setHeaders,
|
||||
"proxy.a.hide_headers": hideHeaders,
|
||||
}}, "")
|
||||
entries, err := p.entriesFromContainerLabels(c)
|
||||
},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 4567, PublicPort: 8888},
|
||||
}}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
a := get(entries, "a")
|
||||
b := get(entries, "b")
|
||||
|
||||
a, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectEqual(t, a.Scheme, "https")
|
||||
ExpectEqual(t, b.Scheme, "https")
|
||||
@@ -77,8 +79,8 @@ X_Custom_Header2: value3
|
||||
ExpectEqual(t, a.Host, "app")
|
||||
ExpectEqual(t, b.Host, "app")
|
||||
|
||||
ExpectEqual(t, a.Port, "4567")
|
||||
ExpectEqual(t, b.Port, "4567")
|
||||
ExpectEqual(t, a.Port, "8888")
|
||||
ExpectEqual(t, b.Port, "8888")
|
||||
|
||||
ExpectTrue(t, a.NoTLSVerify)
|
||||
ExpectTrue(t, b.NoTLSVerify)
|
||||
@@ -110,36 +112,68 @@ X_Custom_Header2: value3
|
||||
|
||||
func TestApplyLabel(t *testing.T) {
|
||||
var p DockerProvider
|
||||
var c = D.FromDocker(&types.Container{
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LableAliases: "a,b,c",
|
||||
D.LabelAliases: "a,b,c",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
"proxy.a.port": "3333",
|
||||
"proxy.b.port": "1234",
|
||||
"proxy.c.scheme": "https",
|
||||
}}, "")
|
||||
entries, err := p.entriesFromContainerLabels(c)
|
||||
},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 3333, PublicPort: 1111},
|
||||
{Type: "tcp", PrivatePort: 4444, PublicPort: 1234},
|
||||
}}, "",
|
||||
))
|
||||
a, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
ExpectTrue(t, ok)
|
||||
c, ok := entries.Load("c")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectEqual(t, get(entries, "a").NoTLSVerify, true)
|
||||
ExpectEqual(t, get(entries, "b").Port, "1234")
|
||||
ExpectEqual(t, get(entries, "c").Scheme, "https")
|
||||
ExpectEqual(t, a.Scheme, "http")
|
||||
ExpectEqual(t, a.Port, "1111")
|
||||
ExpectEqual(t, a.NoTLSVerify, true)
|
||||
ExpectEqual(t, b.Scheme, "http")
|
||||
ExpectEqual(t, b.Port, "1234")
|
||||
ExpectEqual(t, c.Scheme, "https")
|
||||
ExpectEqual(t, c.Port, "1111")
|
||||
}
|
||||
|
||||
func TestApplyLabelWithRef(t *testing.T) {
|
||||
var p DockerProvider
|
||||
var c = D.FromDocker(&types.Container{
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LableAliases: "a,b,c",
|
||||
D.LabelAliases: "a,b,c",
|
||||
"proxy.$1.host": "localhost",
|
||||
"proxy.$2.port": "1234",
|
||||
"proxy.*.port": "1111",
|
||||
"proxy.$1.port": "4444",
|
||||
"proxy.$2.port": "9999",
|
||||
"proxy.$3.scheme": "https",
|
||||
}}, "")
|
||||
entries, err := p.entriesFromContainerLabels(c)
|
||||
},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 3333, PublicPort: 9999},
|
||||
{Type: "tcp", PrivatePort: 4444, PublicPort: 5555},
|
||||
{Type: "tcp", PrivatePort: 1111, PublicPort: 2222},
|
||||
}}, ""))
|
||||
a, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
ExpectTrue(t, ok)
|
||||
c, ok := entries.Load("c")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectEqual(t, get(entries, "a").Host, "localhost")
|
||||
ExpectEqual(t, get(entries, "b").Port, "1234")
|
||||
ExpectEqual(t, get(entries, "c").Scheme, "https")
|
||||
ExpectEqual(t, a.Scheme, "http")
|
||||
ExpectEqual(t, a.Host, "localhost")
|
||||
ExpectEqual(t, a.Port, "5555")
|
||||
ExpectEqual(t, b.Port, "9999")
|
||||
ExpectEqual(t, c.Scheme, "https")
|
||||
ExpectEqual(t, c.Port, "2222")
|
||||
}
|
||||
|
||||
func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||
@@ -147,21 +181,131 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||
var c = D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LableAliases: "a,b",
|
||||
D.LabelAliases: "a,b",
|
||||
"proxy.$1.host": "localhost",
|
||||
"proxy.$4.scheme": "https",
|
||||
}}, "")
|
||||
_, err := p.entriesFromContainerLabels(c)
|
||||
ExpectError(t, E.ErrInvalid, err.Error())
|
||||
ExpectError(t, E.ErrOutOfRange, err.Error())
|
||||
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
||||
|
||||
c = D.FromDocker(&types.Container{
|
||||
_, err = p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LableAliases: "a,b",
|
||||
D.LabelAliases: "a,b",
|
||||
"proxy.$0.host": "localhost",
|
||||
}}, "")
|
||||
_, err = p.entriesFromContainerLabels(c)
|
||||
ExpectError(t, E.ErrInvalid, err.Error())
|
||||
}}, ""))
|
||||
ExpectError(t, E.ErrOutOfRange, err.Error())
|
||||
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
||||
}
|
||||
|
||||
func TestStreamDefaultValues(t *testing.T) {
|
||||
var p DockerProvider
|
||||
var c = D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a",
|
||||
"proxy.*.no_tls_verify": "true",
|
||||
},
|
||||
Ports: []types.Port{
|
||||
{Type: "udp", PrivatePort: 1234, PublicPort: 5678},
|
||||
}}, "",
|
||||
)
|
||||
entries, err := p.entriesFromContainerLabels(c)
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
raw, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
entry, err := P.ValidateEntry(raw)
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
a := ExpectType[*P.StreamEntry](t, entry)
|
||||
ExpectEqual(t, a.Scheme.ListeningScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Scheme.ProxyScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Port.ListeningPort, 0)
|
||||
ExpectEqual(t, a.Port.ProxyPort, 5678)
|
||||
}
|
||||
|
||||
func TestExplicitExclude(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a",
|
||||
D.LabelExclude: "true",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
}}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestImplicitExclude(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
}}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestImplicitExcludeNoExposedPort(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Image: "redis",
|
||||
Names: []string{"redis"},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
},
|
||||
}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("redis")
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestExcludeNonExposedPort(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Image: "redis",
|
||||
Names: []string{"redis"},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"proxy.redis.port": "6379:6379", // should be excluded even specified
|
||||
},
|
||||
}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("redis")
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestNotExcludeNonExposedPortHostNetwork(t *testing.T) {
|
||||
var p DockerProvider
|
||||
cont := &types.Container{
|
||||
Image: "redis",
|
||||
Names: []string{"redis"},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"proxy.redis.port": "6379:6379", // should be excluded even specified
|
||||
},
|
||||
}
|
||||
cont.HostConfig.NetworkMode = "host"
|
||||
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(cont, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("redis")
|
||||
ExpectTrue(t, ok)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,11 @@ func FileProviderImpl(filename string) (ProviderImpl, E.NestedError) {
|
||||
}
|
||||
|
||||
func Validate(data []byte) E.NestedError {
|
||||
return U.ValidateYaml(U.GetSchema(common.ProvidersSchemaPath), data)
|
||||
return U.ValidateYaml(U.GetSchema(common.FileProviderSchemaPath), data)
|
||||
}
|
||||
|
||||
func (p FileProvider) String() string {
|
||||
return p.fileName
|
||||
}
|
||||
|
||||
func (p FileProvider) OnEvent(event W.Event, routes R.Routes) (res EventResult) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -13,7 +12,7 @@ import (
|
||||
|
||||
type (
|
||||
Provider struct {
|
||||
ProviderImpl
|
||||
ProviderImpl `json:"-"`
|
||||
|
||||
name string
|
||||
t ProviderType
|
||||
@@ -30,6 +29,7 @@ type (
|
||||
// even returns error, routes must be non-nil
|
||||
LoadRoutesImpl() (R.Routes, E.NestedError)
|
||||
OnEvent(event W.Event, routes R.Routes) EventResult
|
||||
String() string
|
||||
}
|
||||
ProviderType string
|
||||
EventResult struct {
|
||||
@@ -83,8 +83,9 @@ func (p *Provider) GetType() ProviderType {
|
||||
return p.t
|
||||
}
|
||||
|
||||
func (p *Provider) String() string {
|
||||
return fmt.Sprintf("%s-%s", p.t, p.name)
|
||||
// to work with json marshaller
|
||||
func (p *Provider) MarshalText() ([]byte, error) {
|
||||
return []byte(p.String()), nil
|
||||
}
|
||||
|
||||
func (p *Provider) StartAllRoutes() (res E.NestedError) {
|
||||
@@ -92,7 +93,6 @@ func (p *Provider) StartAllRoutes() (res E.NestedError) {
|
||||
defer errors.To(&res)
|
||||
|
||||
// start watcher no matter load success or not
|
||||
p.watcherCtx, p.watcherCancel = context.WithCancel(context.Background())
|
||||
go p.watchEvents()
|
||||
|
||||
nStarted := 0
|
||||
@@ -153,6 +153,7 @@ func (p *Provider) LoadRoutes() E.NestedError {
|
||||
}
|
||||
|
||||
func (p *Provider) watchEvents() {
|
||||
p.watcherCtx, p.watcherCancel = context.WithCancel(context.Background())
|
||||
events, errs := p.watcher.Events(p.watcherCtx)
|
||||
l := p.l.WithField("module", "watcher")
|
||||
|
||||
@@ -160,21 +161,15 @@ func (p *Provider) watchEvents() {
|
||||
select {
|
||||
case <-p.watcherCtx.Done():
|
||||
return
|
||||
case event, ok := <-events:
|
||||
if !ok { // channel closed
|
||||
return
|
||||
}
|
||||
case event := <-events:
|
||||
res := p.OnEvent(event, p.routes)
|
||||
l.Infof("%s event %q", event.Type, event)
|
||||
l.Infof("%d route added, %d routes removed", res.nAdded, res.nRemoved)
|
||||
if res.err.HasError() {
|
||||
l.Error(res.err)
|
||||
}
|
||||
case err, ok := <-errs:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err.Is(context.Canceled) {
|
||||
case err := <-errs:
|
||||
if err == nil || err.Is(context.Canceled) {
|
||||
continue
|
||||
}
|
||||
l.Errorf("watcher error: %s", err)
|
||||
|
||||
@@ -232,7 +232,7 @@ func NewReverseProxy(target *url.URL, transport http.RoundTripper, entry *Revers
|
||||
}
|
||||
return &ReverseProxy{Rewrite: func(pr *ProxyRequest) {
|
||||
rewriteRequestURL(pr.Out, target)
|
||||
pr.SetXForwarded()
|
||||
// pr.SetXForwarded()
|
||||
setHeaders(pr.Out)
|
||||
hideHeaders(pr.Out)
|
||||
}, Transport: transport}
|
||||
@@ -348,9 +348,9 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
outreq.Header.Del("Forwarded")
|
||||
// outreq.Header.Del("X-Forwarded-For")
|
||||
// outreq.Header.Del("X-Forwarded-Host")
|
||||
// outreq.Header.Del("X-Forwarded-Proto")
|
||||
outreq.Header.Del("X-Forwarded-For")
|
||||
outreq.Header.Del("X-Forwarded-Host")
|
||||
outreq.Header.Del("X-Forwarded-Proto")
|
||||
|
||||
pr := &ProxyRequest{
|
||||
In: req,
|
||||
|
||||
@@ -4,5 +4,5 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const udpBufferSize = 1500
|
||||
const udpBufferSize = 8192
|
||||
const streamStopListenTimeout = 1 * time.Second
|
||||
|
||||
@@ -37,19 +37,23 @@ type (
|
||||
)
|
||||
|
||||
func NewHTTPRoute(entry *P.ReverseProxyEntry) (*HTTPRoute, E.NestedError) {
|
||||
var trans http.RoundTripper
|
||||
var trans *http.Transport
|
||||
var regIdleWatcher func() E.NestedError
|
||||
var unregIdleWatcher func()
|
||||
|
||||
if entry.NoTLSVerify {
|
||||
trans = transportNoTLS
|
||||
trans = transportNoTLS.Clone()
|
||||
} else {
|
||||
trans = transport
|
||||
trans = transport.Clone()
|
||||
}
|
||||
|
||||
rp := P.NewReverseProxy(entry.URL, trans, entry)
|
||||
|
||||
if entry.UseIdleWatcher() {
|
||||
// allow time for response header up to `WakeTimeout`
|
||||
if entry.WakeTimeout > trans.ResponseHeaderTimeout {
|
||||
trans.ResponseHeaderTimeout = entry.WakeTimeout
|
||||
}
|
||||
regIdleWatcher = func() E.NestedError {
|
||||
watcher, err := idlewatcher.Register(entry)
|
||||
if err.HasError() {
|
||||
@@ -114,6 +118,7 @@ func (r *HTTPRoute) Stop() E.NestedError {
|
||||
|
||||
if r.unregIdleWatcher != nil {
|
||||
r.unregIdleWatcher()
|
||||
r.unregIdleWatcher = nil
|
||||
}
|
||||
|
||||
r.mux = nil
|
||||
@@ -143,7 +148,12 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func findMux(host string) (*http.ServeMux, E.NestedError) {
|
||||
sd := strings.Split(host, ".")[0]
|
||||
hostSplit := strings.Split(host, ".")
|
||||
n := len(hostSplit)
|
||||
if n <= 2 {
|
||||
return nil, E.Missing("subdomain")
|
||||
}
|
||||
sd := strings.Join(hostSplit[:n-2], ".")
|
||||
if r, ok := httpRoutes.Load(PT.Alias(sd)); ok {
|
||||
return r.mux, nil
|
||||
}
|
||||
@@ -151,13 +161,13 @@ func findMux(host string) (*http.ServeMux, E.NestedError) {
|
||||
}
|
||||
|
||||
var (
|
||||
defaultDialer = net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
}
|
||||
transport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 1000,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: defaultDialer.DialContext,
|
||||
MaxIdleConnsPerHost: 1000,
|
||||
}
|
||||
transportNoTLS = func() *http.Transport {
|
||||
|
||||
@@ -17,15 +17,15 @@ type (
|
||||
Type() RouteType
|
||||
URL() *url.URL
|
||||
}
|
||||
Routes = F.Map[string, Route]
|
||||
RouteType string
|
||||
Routes = F.Map[string, Route]
|
||||
|
||||
RouteImpl interface {
|
||||
Start() E.NestedError
|
||||
Stop() E.NestedError
|
||||
String() string
|
||||
}
|
||||
route struct {
|
||||
RouteType string
|
||||
route struct {
|
||||
RouteImpl
|
||||
type_ RouteType
|
||||
entry *M.RawEntry
|
||||
@@ -42,7 +42,7 @@ var NewRoutes = F.NewMapOf[string, Route]
|
||||
|
||||
func NewRoute(en *M.RawEntry) (Route, E.NestedError) {
|
||||
rt, err := P.ValidateEntry(en)
|
||||
if err.HasError() {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -58,7 +58,10 @@ func NewRoute(en *M.RawEntry) (Route, E.NestedError) {
|
||||
default:
|
||||
panic("bug: should not reach here")
|
||||
}
|
||||
return &route{RouteImpl: rt.(RouteImpl), entry: en, type_: t}, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &route{RouteImpl: rt.(RouteImpl), entry: en, type_: t}, nil
|
||||
}
|
||||
|
||||
func (rt *route) Entry() *M.RawEntry {
|
||||
|
||||
@@ -2,6 +2,7 @@ package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -13,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
type StreamRoute struct {
|
||||
P.StreamEntry
|
||||
*P.StreamEntry
|
||||
StreamImpl `json:"-"`
|
||||
|
||||
wg sync.WaitGroup
|
||||
@@ -30,6 +31,7 @@ type StreamImpl interface {
|
||||
Accept() (any, error)
|
||||
Handle(any) error
|
||||
CloseListeners()
|
||||
String() string
|
||||
}
|
||||
|
||||
func NewStreamRoute(entry *P.StreamEntry) (*StreamRoute, E.NestedError) {
|
||||
@@ -38,7 +40,7 @@ func NewStreamRoute(entry *P.StreamEntry) (*StreamRoute, E.NestedError) {
|
||||
return nil, E.Unsupported("scheme", fmt.Sprintf("%v -> %v", entry.Scheme.ListeningScheme, entry.Scheme.ProxyScheme))
|
||||
}
|
||||
base := &StreamRoute{
|
||||
StreamEntry: *entry,
|
||||
StreamEntry: entry,
|
||||
connCh: make(chan any, 100),
|
||||
}
|
||||
if entry.Scheme.ListeningScheme.IsTCP() {
|
||||
@@ -63,6 +65,7 @@ func (r *StreamRoute) Start() E.NestedError {
|
||||
if err := r.Setup(); err != nil {
|
||||
return E.FailWith("setup", err)
|
||||
}
|
||||
r.l.Infof("listening on port %d", r.Port.ListeningPort)
|
||||
r.started.Store(true)
|
||||
r.wg.Add(2)
|
||||
go r.grAcceptConnections()
|
||||
@@ -129,7 +132,7 @@ func (r *StreamRoute) grHandleConnections() {
|
||||
case conn := <-r.connCh:
|
||||
go func() {
|
||||
err := r.Handle(conn)
|
||||
if err != nil {
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
r.l.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -5,22 +5,24 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
T "github.com/yusing/go-proxy/proxy/fields"
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
)
|
||||
|
||||
const tcpDialTimeout = 5 * time.Second
|
||||
|
||||
type Pipes []*U.BidirectionalPipe
|
||||
type (
|
||||
Pipes []U.BidirectionalPipe
|
||||
|
||||
type TCPRoute struct {
|
||||
*StreamRoute
|
||||
listener net.Listener
|
||||
pipe Pipes
|
||||
mu sync.Mutex
|
||||
}
|
||||
TCPRoute struct {
|
||||
*StreamRoute
|
||||
listener net.Listener
|
||||
pipe Pipes
|
||||
mu sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
func NewTCPRoute(base *StreamRoute) StreamImpl {
|
||||
return &TCPRoute{
|
||||
@@ -34,6 +36,8 @@ func (route *TCPRoute) Setup() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//! this read the allocated port from orginal ':0'
|
||||
route.Port.ListeningPort = T.Port(in.Addr().(*net.TCPAddr).Port)
|
||||
route.listener = in
|
||||
return nil
|
||||
}
|
||||
@@ -59,10 +63,11 @@ func (route *TCPRoute) Handle(c any) error {
|
||||
}
|
||||
|
||||
route.mu.Lock()
|
||||
defer route.mu.Unlock()
|
||||
|
||||
pipe := U.NewBidirectionalPipe(route.ctx, clientConn, serverConn)
|
||||
route.pipe = append(route.pipe, pipe)
|
||||
|
||||
route.mu.Unlock()
|
||||
return pipe.Start()
|
||||
}
|
||||
|
||||
@@ -72,16 +77,4 @@ func (route *TCPRoute) CloseListeners() {
|
||||
}
|
||||
route.listener.Close()
|
||||
route.listener = nil
|
||||
for _, pipe := range route.pipe {
|
||||
if err := pipe.Stop(); err != nil {
|
||||
switch err {
|
||||
// target closing connection
|
||||
// TODO: handle this by fixing utils/io.go
|
||||
case net.ErrClosed, syscall.EPIPE:
|
||||
return
|
||||
default:
|
||||
route.l.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,33 +4,35 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/yusing/go-proxy/utils"
|
||||
T "github.com/yusing/go-proxy/proxy/fields"
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
)
|
||||
|
||||
type UDPRoute struct {
|
||||
*StreamRoute
|
||||
type (
|
||||
UDPRoute struct {
|
||||
*StreamRoute
|
||||
|
||||
connMap UDPConnMap
|
||||
connMapMutex sync.Mutex
|
||||
connMap UDPConnMap
|
||||
|
||||
listeningConn *net.UDPConn
|
||||
targetAddr *net.UDPAddr
|
||||
}
|
||||
listeningConn *net.UDPConn
|
||||
targetAddr *net.UDPAddr
|
||||
}
|
||||
UDPConn struct {
|
||||
src *net.UDPConn
|
||||
dst *net.UDPConn
|
||||
U.BidirectionalPipe
|
||||
}
|
||||
UDPConnMap = F.Map[string, *UDPConn]
|
||||
)
|
||||
|
||||
type UDPConn struct {
|
||||
src *net.UDPConn
|
||||
dst *net.UDPConn
|
||||
*utils.BidirectionalPipe
|
||||
}
|
||||
|
||||
type UDPConnMap map[string]*UDPConn
|
||||
var NewUDPConnMap = F.NewMapOf[string, *UDPConn]
|
||||
|
||||
func NewUDPRoute(base *StreamRoute) StreamImpl {
|
||||
return &UDPRoute{
|
||||
StreamRoute: base,
|
||||
connMap: make(UDPConnMap),
|
||||
connMap: NewUDPConnMap(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +51,12 @@ func (route *UDPRoute) Setup() error {
|
||||
return err
|
||||
}
|
||||
|
||||
//! this read the allocated listeningPort from orginal ':0'
|
||||
route.Port.ListeningPort = T.Port(source.LocalAddr().(*net.UDPAddr).Port)
|
||||
|
||||
route.listeningConn = source
|
||||
route.targetAddr = raddr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -69,28 +75,24 @@ func (route *UDPRoute) Accept() (any, error) {
|
||||
}
|
||||
|
||||
key := srcAddr.String()
|
||||
conn, ok := route.connMap[key]
|
||||
conn, ok := route.connMap.Load(key)
|
||||
|
||||
if !ok {
|
||||
route.connMapMutex.Lock()
|
||||
if conn, ok = route.connMap[key]; !ok {
|
||||
srcConn, err := net.DialUDP("udp", nil, srcAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dstConn, err := net.DialUDP("udp", nil, route.targetAddr)
|
||||
if err != nil {
|
||||
srcConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
conn = &UDPConn{
|
||||
srcConn,
|
||||
dstConn,
|
||||
utils.NewBidirectionalPipe(route.ctx, sourceRWCloser{in, dstConn}, sourceRWCloser{in, srcConn}),
|
||||
}
|
||||
route.connMap[key] = conn
|
||||
srcConn, err := net.DialUDP("udp", nil, srcAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
route.connMapMutex.Unlock()
|
||||
dstConn, err := net.DialUDP("udp", nil, route.targetAddr)
|
||||
if err != nil {
|
||||
srcConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
conn = &UDPConn{
|
||||
srcConn,
|
||||
dstConn,
|
||||
U.NewBidirectionalPipe(route.ctx, sourceRWCloser{in, dstConn}, sourceRWCloser{in, srcConn}),
|
||||
}
|
||||
route.connMap.Store(key, conn)
|
||||
}
|
||||
|
||||
_, err = conn.dst.Write(buffer[:nRead])
|
||||
@@ -106,15 +108,15 @@ func (route *UDPRoute) CloseListeners() {
|
||||
route.listeningConn.Close()
|
||||
route.listeningConn = nil
|
||||
}
|
||||
for _, conn := range route.connMap {
|
||||
route.connMap.RangeAll(func(_ string, conn *UDPConn) {
|
||||
if err := conn.src.Close(); err != nil {
|
||||
route.l.Errorf("error closing src conn: %s", err)
|
||||
}
|
||||
if err := conn.dst.Close(); err != nil {
|
||||
route.l.Error("error closing dst conn: %s", err)
|
||||
}
|
||||
}
|
||||
route.connMap = make(UDPConnMap)
|
||||
})
|
||||
route.connMap.Clear()
|
||||
}
|
||||
|
||||
type sourceRWCloser struct {
|
||||
|
||||
115
src/setup.go
Normal file
115
src/setup.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
. "github.com/yusing/go-proxy/common"
|
||||
)
|
||||
|
||||
var branch = GetEnv("GOPROXY_BRANCH", "v0.5")
|
||||
var baseUrl = fmt.Sprintf("https://github.com/yusing/go-proxy/raw/%s", branch)
|
||||
var requiredConfigs = []Config{
|
||||
{ConfigBasePath, true, false, ""},
|
||||
{ComposeFileName, false, true, ComposeExampleFileName},
|
||||
{path.Join(ConfigBasePath, ConfigFileName), false, true, ConfigExampleFileName},
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Pathname string
|
||||
IsDir bool
|
||||
NeedDownload bool
|
||||
DownloadFileName string
|
||||
}
|
||||
|
||||
func Setup() {
|
||||
log.Println("setting up go-proxy")
|
||||
log.Println("branch:", branch)
|
||||
|
||||
os.Chdir("/setup")
|
||||
|
||||
for _, config := range requiredConfigs {
|
||||
config.setup()
|
||||
}
|
||||
|
||||
log.Println("done")
|
||||
}
|
||||
|
||||
func (c *Config) setup() {
|
||||
if c.IsDir {
|
||||
mkdir(c.Pathname)
|
||||
return
|
||||
}
|
||||
if !c.NeedDownload {
|
||||
touch(c.Pathname)
|
||||
return
|
||||
}
|
||||
|
||||
fetch(c.DownloadFileName, c.Pathname)
|
||||
}
|
||||
|
||||
func hasFileOrDir(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func mkdir(pathname string) {
|
||||
_, err := os.Stat(pathname)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
log.Printf("creating directory %q\n", pathname)
|
||||
err := os.MkdirAll(pathname, 0o755)
|
||||
if err != nil {
|
||||
log.Fatalf("failed: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func touch(pathname string) {
|
||||
if hasFileOrDir(pathname) {
|
||||
return
|
||||
}
|
||||
log.Printf("creating file %q\n", pathname)
|
||||
_, err := os.Create(pathname)
|
||||
if err != nil {
|
||||
log.Fatalf("failed: %s\n", err)
|
||||
}
|
||||
}
|
||||
func fetch(remoteFilename string, outFileName string) {
|
||||
if hasFileOrDir(outFileName) {
|
||||
return
|
||||
}
|
||||
log.Printf("downloading %q\n", remoteFilename)
|
||||
|
||||
url, err := url.JoinPath(baseUrl, remoteFilename)
|
||||
if err != nil {
|
||||
log.Fatalf("unexpected error: %s\n", err)
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Fatalf("http request failed: %s\n", err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalf("error reading response body: %s\n", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(outFileName, body, 0o644)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to write to file: %s\n", err)
|
||||
}
|
||||
|
||||
log.Printf("downloaded %q\n", outFileName)
|
||||
}
|
||||
@@ -3,9 +3,10 @@ package utils
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
@@ -16,15 +17,19 @@ type (
|
||||
Path string
|
||||
}
|
||||
|
||||
ReadCloser struct {
|
||||
ctx context.Context
|
||||
r io.ReadCloser
|
||||
closed atomic.Bool
|
||||
ContextReader struct {
|
||||
ctx context.Context
|
||||
io.Reader
|
||||
}
|
||||
|
||||
ContextWriter struct {
|
||||
ctx context.Context
|
||||
io.Writer
|
||||
}
|
||||
|
||||
Pipe struct {
|
||||
r ReadCloser
|
||||
w io.WriteCloser
|
||||
r ContextReader
|
||||
w ContextWriter
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
@@ -35,48 +40,48 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func (r *ReadCloser) Read(p []byte) (int, error) {
|
||||
func (r *ContextReader) Read(p []byte) (int, error) {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return 0, r.ctx.Err()
|
||||
default:
|
||||
return r.r.Read(p)
|
||||
return r.Reader.Read(p)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReadCloser) Close() error {
|
||||
if r.closed.Load() {
|
||||
return nil
|
||||
func (w *ContextWriter) Write(p []byte) (int, error) {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return 0, w.ctx.Err()
|
||||
default:
|
||||
return w.Writer.Write(p)
|
||||
}
|
||||
r.closed.Store(true)
|
||||
return r.r.Close()
|
||||
}
|
||||
|
||||
func NewPipe(ctx context.Context, r io.ReadCloser, w io.WriteCloser) *Pipe {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
_, cancel := context.WithCancel(ctx)
|
||||
return &Pipe{
|
||||
r: ReadCloser{ctx: ctx, r: r},
|
||||
w: w,
|
||||
r: ContextReader{ctx: ctx, Reader: r},
|
||||
w: ContextWriter{ctx: ctx, Writer: w},
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pipe) Start() error {
|
||||
return Copy(p.ctx, p.w, &p.r)
|
||||
func (p *Pipe) Start() (err error) {
|
||||
err = Copy(&p.w, &p.r)
|
||||
switch {
|
||||
case
|
||||
// NOTE: ignoring broken pipe and connection reset by peer
|
||||
errors.Is(err, syscall.EPIPE),
|
||||
errors.Is(err, syscall.ECONNRESET):
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Pipe) Stop() error {
|
||||
p.cancel()
|
||||
return E.JoinE("error stopping pipe", p.r.Close(), p.w.Close()).Error()
|
||||
}
|
||||
|
||||
func (p *Pipe) Write(b []byte) (int, error) {
|
||||
return p.w.Write(b)
|
||||
}
|
||||
|
||||
func NewBidirectionalPipe(ctx context.Context, rw1 io.ReadWriteCloser, rw2 io.ReadWriteCloser) *BidirectionalPipe {
|
||||
return &BidirectionalPipe{
|
||||
func NewBidirectionalPipe(ctx context.Context, rw1 io.ReadWriteCloser, rw2 io.ReadWriteCloser) BidirectionalPipe {
|
||||
return BidirectionalPipe{
|
||||
pSrcDst: NewPipe(ctx, rw1, rw2),
|
||||
pDstSrc: NewPipe(ctx, rw2, rw1),
|
||||
}
|
||||
@@ -89,7 +94,7 @@ func NewBidirectionalPipeIntermediate(ctx context.Context, listener io.ReadClose
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BidirectionalPipe) Start() error {
|
||||
func (p BidirectionalPipe) Start() error {
|
||||
errCh := make(chan error, 2)
|
||||
go func() {
|
||||
errCh <- p.pSrcDst.Start()
|
||||
@@ -97,20 +102,11 @@ func (p *BidirectionalPipe) Start() error {
|
||||
go func() {
|
||||
errCh <- p.pDstSrc.Start()
|
||||
}()
|
||||
for err := range errCh {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return E.JoinE("bidirectional pipe error", <-errCh, <-errCh).Error()
|
||||
}
|
||||
|
||||
func (p *BidirectionalPipe) Stop() error {
|
||||
return E.JoinE("error stopping pipe", p.pSrcDst.Stop(), p.pDstSrc.Stop()).Error()
|
||||
}
|
||||
|
||||
func Copy(ctx context.Context, dst io.WriteCloser, src io.ReadCloser) error {
|
||||
_, err := io.Copy(dst, &ReadCloser{ctx: ctx, r: src})
|
||||
func Copy(dst *ContextWriter, src *ContextReader) error {
|
||||
_, err := io.Copy(dst, src)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,10 @@ import (
|
||||
"github.com/santhosh-tekuri/jsonschema"
|
||||
)
|
||||
|
||||
var schemaCompiler = func() *jsonschema.Compiler {
|
||||
c := jsonschema.NewCompiler()
|
||||
c.Draft = jsonschema.Draft7
|
||||
return c
|
||||
}()
|
||||
|
||||
var schemaStorage = make(map[string]*jsonschema.Schema)
|
||||
var (
|
||||
schemaCompiler = jsonschema.NewCompiler()
|
||||
schemaStorage = make(map[string]*jsonschema.Schema)
|
||||
)
|
||||
|
||||
func GetSchema(path string) *jsonschema.Schema {
|
||||
if schema, ok := schemaStorage[path]; ok {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CommaSeperatedList(s string) []string {
|
||||
res := strings.Split(s, ",")
|
||||
@@ -9,3 +13,11 @@ func CommaSeperatedList(s string) []string {
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func ExtractPort(fullURL string) (int, error) {
|
||||
url, err := url.Parse(fullURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(url.Port())
|
||||
}
|
||||
|
||||
@@ -10,13 +10,23 @@ func ExpectNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil && !reflect.ValueOf(err).IsNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectError(t *testing.T, expected error, err error) {
|
||||
t.Helper()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Errorf("expected err %s, got nil", expected.Error())
|
||||
t.Errorf("expected err %s, got %s", expected.Error(), err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectError2(t *testing.T, input any, expected error, err error) {
|
||||
t.Helper()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Errorf("%v: expected err %s, got %s", input, expected.Error(), err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +34,7 @@ func ExpectEqual[T comparable](t *testing.T, got T, want T) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("expected:\n%v, got\n%v", want, got)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,29 +42,34 @@ func ExpectDeepEqual[T any](t *testing.T, got T, want T) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("expected:\n%v, got\n%v", want, got)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectTrue(t *testing.T, got bool) {
|
||||
t.Helper()
|
||||
if !got {
|
||||
t.Errorf("expected true, got false")
|
||||
t.Error("expected true")
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectFalse(t *testing.T, got bool) {
|
||||
t.Helper()
|
||||
if got {
|
||||
t.Errorf("expected false, got true")
|
||||
t.Error("expected false")
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectType[T any](t *testing.T, got any) T {
|
||||
func ExpectType[T any](t *testing.T, got any) (_ T) {
|
||||
t.Helper()
|
||||
tExpect := reflect.TypeFor[T]()
|
||||
_, ok := got.(T)
|
||||
if !ok {
|
||||
t.Errorf("expected type %s, got %T", tExpect, got)
|
||||
t.Fatalf("expected type %s, got %s", tExpect, reflect.TypeOf(got).Elem())
|
||||
t.FailNow()
|
||||
return
|
||||
}
|
||||
return got.(T)
|
||||
}
|
||||
|
||||
@@ -42,11 +42,19 @@ func DockerrFilterContainerName(name string) filters.KeyValuePair {
|
||||
}
|
||||
|
||||
func NewDockerWatcher(host string) DockerWatcher {
|
||||
return DockerWatcher{host: host, FieldLogger: logrus.WithField("module", "docker_watcher")}
|
||||
return DockerWatcher{
|
||||
host: host,
|
||||
FieldLogger: (logrus.
|
||||
WithField("module", "docker_watcher").
|
||||
WithField("host", host))}
|
||||
}
|
||||
|
||||
func NewDockerWatcherWithClient(client D.Client) DockerWatcher {
|
||||
return DockerWatcher{client: client, FieldLogger: logrus.WithField("module", "docker_watcher")}
|
||||
return DockerWatcher{
|
||||
client: client,
|
||||
FieldLogger: (logrus.
|
||||
WithField("module", "docker_watcher").
|
||||
WithField("host", client.DaemonHost()))}
|
||||
}
|
||||
|
||||
func (w DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.NestedError) {
|
||||
@@ -56,7 +64,6 @@ func (w DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Neste
|
||||
func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerListOptions) (<-chan Event, <-chan E.NestedError) {
|
||||
eventCh := make(chan Event)
|
||||
errCh := make(chan E.NestedError)
|
||||
started := make(chan struct{})
|
||||
|
||||
eventsCtx, eventsCancel := context.WithCancel(ctx)
|
||||
|
||||
@@ -75,7 +82,7 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
|
||||
attempts := 0
|
||||
for {
|
||||
w.client, err = D.ConnectClient(w.host)
|
||||
if err != nil {
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
attempts++
|
||||
@@ -89,8 +96,11 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
|
||||
}
|
||||
}
|
||||
|
||||
w.Debugf("client connected")
|
||||
|
||||
cEventCh, cErrCh := w.client.Events(eventsCtx, options)
|
||||
started <- struct{}{}
|
||||
|
||||
w.Debugf("watcher started")
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -130,7 +140,6 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
|
||||
}
|
||||
}
|
||||
}()
|
||||
<-started
|
||||
|
||||
return eventCh, errCh
|
||||
}
|
||||
|
||||
@@ -14,36 +14,64 @@ type (
|
||||
ActorAttributes map[string]string
|
||||
Action Action
|
||||
}
|
||||
Action string
|
||||
Action uint16
|
||||
EventType string
|
||||
)
|
||||
|
||||
const (
|
||||
ActionFileModified Action = "modified"
|
||||
ActionFileCreated Action = "created"
|
||||
ActionFileDeleted Action = "deleted"
|
||||
ActionFileModified Action = (1 << iota)
|
||||
ActionFileCreated
|
||||
ActionFileDeleted
|
||||
|
||||
ActionDockerStartUnpause Action = "start"
|
||||
ActionDockerStopPause Action = "stop"
|
||||
ActionContainerCreate
|
||||
ActionContainerStart
|
||||
ActionContainerUnpause
|
||||
|
||||
ActionContainerKill
|
||||
ActionContainerStop
|
||||
ActionContainerPause
|
||||
ActionContainerDie
|
||||
|
||||
actionContainerWakeMask = ActionContainerCreate | ActionContainerStart | ActionContainerUnpause
|
||||
actionContainerSleepMask = ActionContainerKill | ActionContainerStop | ActionContainerPause | ActionContainerDie
|
||||
)
|
||||
|
||||
const (
|
||||
EventTypeDocker EventType = "docker"
|
||||
EventTypeFile EventType = "file"
|
||||
)
|
||||
|
||||
var DockerEventMap = map[dockerEvents.Action]Action{
|
||||
dockerEvents.ActionCreate: ActionDockerStartUnpause,
|
||||
dockerEvents.ActionStart: ActionDockerStartUnpause,
|
||||
dockerEvents.ActionPause: ActionDockerStartUnpause,
|
||||
dockerEvents.ActionDie: ActionDockerStopPause,
|
||||
dockerEvents.ActionStop: ActionDockerStopPause,
|
||||
dockerEvents.ActionUnPause: ActionDockerStopPause,
|
||||
dockerEvents.ActionKill: ActionDockerStopPause,
|
||||
dockerEvents.ActionCreate: ActionContainerCreate,
|
||||
dockerEvents.ActionStart: ActionContainerStart,
|
||||
dockerEvents.ActionUnPause: ActionContainerUnpause,
|
||||
|
||||
dockerEvents.ActionKill: ActionContainerKill,
|
||||
dockerEvents.ActionStop: ActionContainerStop,
|
||||
dockerEvents.ActionPause: ActionContainerPause,
|
||||
dockerEvents.ActionDie: ActionContainerDie,
|
||||
}
|
||||
|
||||
var dockerActionNameMap = func() (m map[Action]string) {
|
||||
m = make(map[Action]string, len(DockerEventMap))
|
||||
for k, v := range DockerEventMap {
|
||||
m[v] = string(k)
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
func (e Event) String() string {
|
||||
return fmt.Sprintf("%s %s", e.ActorName, e.Action)
|
||||
}
|
||||
|
||||
func (a Action) IsDelete() bool {
|
||||
return a == ActionFileDeleted
|
||||
func (a Action) String() string {
|
||||
return dockerActionNameMap[a]
|
||||
}
|
||||
|
||||
func (a Action) IsContainerWake() bool {
|
||||
return a&actionContainerWakeMask != 0
|
||||
}
|
||||
|
||||
func (a Action) IsContainerSleep() bool {
|
||||
return a&actionContainerSleepMask != 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user