mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-10 02:47:44 +01:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
415f169f48 | ||
|
|
e2b08d8667 | ||
|
|
91e7f4894a | ||
|
|
a78dba5191 | ||
|
|
c7208c90c6 | ||
|
|
da6a2756fa | ||
|
|
9a6a66f5a8 | ||
|
|
90487bfde6 | ||
|
|
4120fd8d1c | ||
|
|
6f3a5ebe6e | ||
|
|
a935f200a3 | ||
|
|
f474ae4f75 | ||
|
|
345a4417a6 | ||
|
|
8cca83723c | ||
|
|
aa2fcd47c2 | ||
|
|
0580a7d3cd | ||
|
|
a43c242c66 | ||
|
|
45d4b92fc6 | ||
|
|
72df9ff3e4 | ||
|
|
48bf31fd0e | ||
|
|
4ee5383f7d | ||
|
|
33fb60a32d | ||
|
|
d10d0e49fa | ||
|
|
dc3575c8fd | ||
|
|
17115cfb0b | ||
|
|
498082f7e5 | ||
|
|
99216ffe59 | ||
|
|
f426dbc9cf | ||
|
|
1c611cc9b9 | ||
|
|
dc43e26770 | ||
|
|
79ae26f1b5 |
145
.github/workflows/docker-image.yml
vendored
145
.github/workflows/docker-image.yml
vendored
@@ -1,21 +1,132 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
jobs:
|
||||
build_and_push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Docker Build and Push
|
||||
id: docker_build_push
|
||||
uses: GlueOps/github-actions-build-push-containers@v0.3.7
|
||||
with:
|
||||
tags: ${{ github.ref_name }}
|
||||
push:
|
||||
tags: ["*"]
|
||||
|
||||
- name: Tag as latest
|
||||
if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-')
|
||||
run: |
|
||||
docker tag ghcr.io/${{ github.repository }}:${{ github.ref_name }} ghcr.io/${{ github.repository }}:latest
|
||||
docker push ghcr.io/${{ github.repository }}:latest
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build multi-platform Docker image
|
||||
runs-on: self-hosted
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
merge:
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- build
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
id: push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
- name: Tag as latest
|
||||
if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-')
|
||||
run: |
|
||||
docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -3,8 +3,7 @@ compose.yml
|
||||
config*/
|
||||
certs*/
|
||||
bin/
|
||||
|
||||
templates/codemirror/
|
||||
error_pages/
|
||||
|
||||
logs/
|
||||
log/
|
||||
@@ -13,8 +12,10 @@ log/
|
||||
|
||||
go.work.sum
|
||||
|
||||
!src/**/
|
||||
!cmd/**/
|
||||
!internal/**/
|
||||
|
||||
todo.md
|
||||
|
||||
.*.swp
|
||||
.*.swp
|
||||
.aider*
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "frontend"]
|
||||
path = frontend
|
||||
url = https://github.com/yusing/go-proxy-frontend
|
||||
|
||||
35
Dockerfile
35
Dockerfile
@@ -1,23 +1,43 @@
|
||||
# Stage 1: Builder
|
||||
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
|
||||
|
||||
# Only copy go.mod and go.sum initially for better caching
|
||||
COPY go.mod go.sum /src
|
||||
|
||||
# Utilize build cache
|
||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||
go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get
|
||||
|
||||
ENV GOCACHE=/root/.cache/go-build
|
||||
|
||||
# Build the application with better caching
|
||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||
--mount=type=cache,target="/root/.cache/go-build" \
|
||||
go mod download && \
|
||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o go-proxy github.com/yusing/go-proxy
|
||||
--mount=type=bind,src=cmd,dst=/src/cmd \
|
||||
--mount=type=bind,src=internal,dst=/src/internal \
|
||||
CGO_ENABLED=0 GOOS=linux go build -ldflags '-w -s' -pgo=auto -o /app/go-proxy ./cmd && \
|
||||
mkdir /app/error_pages /app/certs
|
||||
|
||||
# Stage 2: Final image
|
||||
FROM scratch
|
||||
|
||||
LABEL maintainer="yusing@6uo.me"
|
||||
LABEL proxy.exclude=1
|
||||
|
||||
# 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
|
||||
COPY --from=builder /app /app
|
||||
|
||||
# copy schema directory
|
||||
COPY schema/ /app/schema/
|
||||
|
||||
# copy certs
|
||||
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||
|
||||
ENV DOCKER_HOST=unix:///var/run/docker.sock
|
||||
ENV GOPROXY_DEBUG=0
|
||||
@@ -27,4 +47,5 @@ EXPOSE 8888
|
||||
EXPOSE 443
|
||||
|
||||
WORKDIR /app
|
||||
CMD ["/app/go-proxy"]
|
||||
|
||||
CMD ["/app/go-proxy"]
|
||||
|
||||
24
Makefile
24
Makefile
@@ -1,6 +1,8 @@
|
||||
.PHONY: all build up quick-restart restart logs get udp-server
|
||||
BUILD_FLAG ?= -s -w
|
||||
|
||||
all: build quick-restart logs
|
||||
.PHONY: all setup build test up restart logs get debug run archive repush rapid-crash debug-list-containers
|
||||
|
||||
all: debug
|
||||
|
||||
setup:
|
||||
mkdir -p config certs
|
||||
@@ -9,10 +11,11 @@ setup:
|
||||
|
||||
build:
|
||||
mkdir -p bin
|
||||
CGO_ENABLED=0 GOOS=linux go build -pgo=auto -o bin/go-proxy github.com/yusing/go-proxy
|
||||
CGO_ENABLED=0 GOOS=linux \
|
||||
go build -ldflags '${BUILD_FLAG}' -pgo=auto -o bin/go-proxy ./cmd
|
||||
|
||||
test:
|
||||
go test ./src/...
|
||||
go test ./internal/...
|
||||
|
||||
up:
|
||||
docker compose up -d
|
||||
@@ -24,10 +27,13 @@ logs:
|
||||
docker compose logs -f
|
||||
|
||||
get:
|
||||
cd src && go get -u && go mod tidy && cd ..
|
||||
cd cmd && go get -u && go mod tidy && cd ..
|
||||
|
||||
debug:
|
||||
make build && sudo GOPROXY_DEBUG=1 bin/go-proxy
|
||||
make BUILD_FLAG="" build && sudo GOPROXY_DEBUG=1 bin/go-proxy
|
||||
|
||||
run:
|
||||
make build && sudo bin/go-proxy
|
||||
|
||||
archive:
|
||||
git archive HEAD -o ../go-proxy-$$(date +"%Y%m%d%H%M").zip
|
||||
@@ -44,4 +50,8 @@ rapid-crash:
|
||||
sudo docker rm -f test_crash
|
||||
|
||||
debug-list-containers:
|
||||
bash -c 'echo -e "GET /containers/json HTTP/1.0\r\n" | sudo netcat -U /var/run/docker.sock | tail -n +9 | jq'
|
||||
bash -c 'echo -e "GET /containers/json HTTP/1.0\r\n" | sudo netcat -U /var/run/docker.sock | tail -n +9 | jq'
|
||||
|
||||
ci-test:
|
||||
mkdir -p /tmp/artifacts
|
||||
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
|
||||
66
README.md
66
README.md
@@ -5,6 +5,7 @@
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
[繁體中文文檔請看此](README_CHT.md)
|
||||
|
||||
@@ -23,7 +24,7 @@ A lightweight, easy-to-use, and [performant](docs/benchmark_result.md) reverse p
|
||||
- [Environment variables](#environment-variables)
|
||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
||||
- [Config File](#config-file)
|
||||
- [Provider File](#provider-file)
|
||||
- [Include Files](#include-files)
|
||||
- [Showcase](#showcase)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
- [Build it yourself](#build-it-yourself)
|
||||
@@ -32,14 +33,18 @@ A lightweight, easy-to-use, and [performant](docs/benchmark_result.md) reverse p
|
||||
|
||||
- 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 SSL cert management (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, see [showcase](#idlesleeper))_
|
||||
- **idlesleeper**: stop containers on idle, wake it up on traffic _(optional, see [showcase](#idlesleeper))_
|
||||
- HTTP(s) reserve proxy
|
||||
- [HTTP middleware support](docs/middlewares.md) _(experimental)_
|
||||
- [Custom error pages support](docs/middlewares.md#custom-error-pages)
|
||||
- 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))
|
||||
- Supports linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v6 multi-platform
|
||||
- Written in **[Go](https://go.dev)**
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
@@ -48,18 +53,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)
|
||||
|
||||
@@ -73,7 +89,7 @@ A lightweight, easy-to-use, and [performant](docs/benchmark_result.md) reverse p
|
||||
| `ls-config` | list config and exit | `go-proxy ls-config \| jq` |
|
||||
| `ls-route` | list proxy entries and exit | `go-proxy ls-route \| jq` |
|
||||
|
||||
**run with `docker exec <container_name> /app/go-proxy <command>`**
|
||||
**run with `docker exec go-proxy /app/go-proxy <command>`**
|
||||
|
||||
### Environment variables
|
||||
|
||||
@@ -93,31 +109,13 @@ Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscod
|
||||
|
||||
### Config File
|
||||
|
||||
See [config.example.yml](config.example.yml) for more
|
||||
|
||||
```yaml
|
||||
# autocert configuration
|
||||
autocert:
|
||||
email: # ACME Email
|
||||
domains: # a list of domains for cert registration
|
||||
provider: # DNS Challenge provider
|
||||
options: # provider specific options
|
||||
- ...
|
||||
# reverse proxy providers configuration
|
||||
providers:
|
||||
include:
|
||||
- providers.yml
|
||||
- other_file_1.yml
|
||||
- ...
|
||||
docker:
|
||||
local: $DOCKER_HOST
|
||||
remote-1: tcp://10.0.2.1:2375
|
||||
remote-2: ssh://root:1234@10.0.2.2
|
||||
```
|
||||
See [config.example.yml](config.example.yml)
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Provider File
|
||||
### Include Files
|
||||
|
||||
These are files that include standalone proxy entries
|
||||
|
||||
See [Fields](docs/docker.md#fields)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
一個輕量化、易用且[高效](docs/benchmark_result.md)的反向代理和端口轉發工具
|
||||
|
||||
@@ -30,14 +31,16 @@
|
||||
|
||||
- 易用
|
||||
- 不需花費太多時間就能輕鬆配置
|
||||
- 支持多個docker節點
|
||||
- 除錯簡單
|
||||
- 自動處理 HTTPS 證書(參見[可用的 DNS 供應商](docs/dns_providers.md))
|
||||
- 自動配置 SSL 證書(參見[可用的 DNS 供應商](docs/dns_providers.md))
|
||||
- 透過 Docker 容器自動配置
|
||||
- 容器狀態變更時自動熱重載
|
||||
- 容器閒置時自動暫停/停止,入站時自動喚醒
|
||||
- HTTP(s)反向代理
|
||||
- HTTP(s) 反向代理
|
||||
- TCP/UDP 端口轉發
|
||||
- 用於配置和監控的前端 Web 面板([截圖](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots))
|
||||
- 支持 linux/amd64、linux/arm64、linux/arm/v7、linux/arm/v6 多平台
|
||||
- 使用 **[Go](https://go.dev)** 編寫
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
@@ -46,16 +49,29 @@
|
||||
|
||||
### 安裝
|
||||
|
||||
1. 設置 DNS 記錄,例如:
|
||||
1. 抓取Docker鏡像
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/yusing/go-proxy:latest
|
||||
```
|
||||
|
||||
2. 建立新的目錄,並切換到該目錄,並執行
|
||||
|
||||
```shell
|
||||
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/go-proxy setup
|
||||
```
|
||||
|
||||
3. 設置 DNS 記錄,例如:
|
||||
|
||||
- A 記錄: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA 記錄: `*.y.z` -> `::ffff:a00:a01`
|
||||
|
||||
2. 安裝 `go-proxy` [參見這裡](docs/docker.md)
|
||||
4. 配置 `docker-socket-proxy` 其他 Docker 節點(如有) (參見 [範例](docs/docker_socket_proxy.md)) 然後加到 `config.yml` 中
|
||||
|
||||
3. 配置 `go-proxy`
|
||||
5. 大功告成,你可以做一些額外的配置
|
||||
- 使用文本編輯器 (推薦 Visual Studio Code [參見 VSCode 使用 schema](#vscode-中使用-json-schema))
|
||||
- 或通過 `http://gp.y.z` 使用網頁配置編輯器
|
||||
- 詳情請參閱 [docker.md](docs/docker.md)
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
@@ -69,7 +85,7 @@
|
||||
| `ls-config` | 列出配置並退出 | `go-proxy ls-config \| jq` |
|
||||
| `ls-route` | 列出路由並退出 | `go-proxy ls-route \| jq` |
|
||||
|
||||
**使用 `docker exec <容器名稱> /app/go-proxy <參數>` 運行**
|
||||
**使用 `docker exec go-proxy /app/go-proxy <參數>` 運行**
|
||||
|
||||
### 環境變量
|
||||
|
||||
@@ -89,27 +105,7 @@
|
||||
|
||||
### 配置文件
|
||||
|
||||
參見 [config.example.yml](config.example.yml) 了解更多
|
||||
|
||||
```yaml
|
||||
# autocert 配置
|
||||
autocert:
|
||||
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
|
||||
```
|
||||
參見 [config.example.yml](config.example.yml)
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
|
||||
@@ -16,20 +16,27 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/api"
|
||||
apiUtils "github.com/yusing/go-proxy/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/common"
|
||||
"github.com/yusing/go-proxy/config"
|
||||
"github.com/yusing/go-proxy/docker"
|
||||
"github.com/yusing/go-proxy/docker/idlewatcher"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
"github.com/yusing/go-proxy/server"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
"github.com/yusing/go-proxy/internal"
|
||||
"github.com/yusing/go-proxy/internal/api"
|
||||
apiUtils "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/config"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
"github.com/yusing/go-proxy/internal/docker/idlewatcher"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/server"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
)
|
||||
|
||||
func main() {
|
||||
args := common.GetArgs()
|
||||
|
||||
if args.Command == common.CommandSetup {
|
||||
internal.Setup()
|
||||
return
|
||||
}
|
||||
|
||||
l := logrus.WithField("module", "main")
|
||||
onShutdown := F.NewSlice[func()]()
|
||||
|
||||
@@ -41,11 +48,10 @@ func main() {
|
||||
logrus.SetOutput(io.Discard)
|
||||
} else {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableSorting: true,
|
||||
DisableLevelTruncation: true,
|
||||
FullTimestamp: true,
|
||||
ForceColors: true,
|
||||
TimestampFormat: "01-02 15:04:05",
|
||||
DisableSorting: true,
|
||||
FullTimestamp: true,
|
||||
ForceColors: true,
|
||||
TimestampFormat: "01-02 15:04:05",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -70,11 +76,16 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err.IsFatal() {
|
||||
log.Fatal(err)
|
||||
for _, dir := range common.RequiredDirectories {
|
||||
prepareDirectory(dir)
|
||||
}
|
||||
|
||||
err := config.Load()
|
||||
if err != nil {
|
||||
logrus.Warn(err)
|
||||
}
|
||||
cfg := config.GetInstance()
|
||||
|
||||
switch args.Command {
|
||||
case common.CommandListConfigs:
|
||||
printJSON(cfg.Value())
|
||||
@@ -90,6 +101,10 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if common.IsDebug {
|
||||
printJSON(docker.GetRegisteredNamespaces())
|
||||
}
|
||||
|
||||
cfg.StartProxyProviders()
|
||||
|
||||
if err.HasError() {
|
||||
@@ -110,10 +125,7 @@ func main() {
|
||||
|
||||
if autocert != nil {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
if err = autocert.Setup(ctx); err != nil && err.IsWarning() {
|
||||
cancel()
|
||||
l.Warn(err)
|
||||
} else if err.IsFatal() {
|
||||
if err = autocert.Setup(ctx); err != nil {
|
||||
l.Fatal(err)
|
||||
} else {
|
||||
onShutdown.Add(cancel)
|
||||
@@ -180,13 +192,21 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func prepareDirectory(dir string) {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||
logrus.Fatalf("failed to create directory %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
j, err := E.Check(json.MarshalIndent(obj, "", " "))
|
||||
if err.HasError() {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@@ -1,37 +1,69 @@
|
||||
# Autocert (choose one below and uncomment to enable)
|
||||
|
||||
#
|
||||
# 1. use existing cert
|
||||
#
|
||||
# autocert:
|
||||
# provider: local
|
||||
# cert_path: certs/cert.crt # optional, uncomment only if you need to change it
|
||||
# key_path: certs/priv.key # optional, uncomment only if you need to change it
|
||||
|
||||
#
|
||||
# cert_path: certs/cert.crt # optional, uncomment only if you need to change it
|
||||
# key_path: certs/priv.key # optional, uncomment only if you need to change it
|
||||
#
|
||||
# 2. cloudflare
|
||||
#
|
||||
# autocert:
|
||||
# provider: cloudflare
|
||||
# email: # ACME Email
|
||||
# domains: # a list of domains for cert registration
|
||||
# - x.y.z
|
||||
# email: abc@gmail.com # ACME Email
|
||||
# domains: # a list of domains for cert registration
|
||||
# - "*.y.z" # remember to use double quotes to surround wildcard domain
|
||||
# options:
|
||||
# - auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
|
||||
# 3. other providers, check readme for more
|
||||
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
#
|
||||
# 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 files are standalone yaml files under `config/` directory
|
||||
#
|
||||
# include:
|
||||
# - file1.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
|
||||
# $DOCKER_HOST implies environment variable `DOCKER_HOST` or unix:///var/run/docker.sock by default
|
||||
local: $DOCKER_HOST
|
||||
# explicit only mode
|
||||
# only containers with explicit aliases will be proxied
|
||||
# add "!" after provider name to enable explicit only mode
|
||||
#
|
||||
# local!: $DOCKER_HOST
|
||||
#
|
||||
# add more docker providers if needed
|
||||
# for value format, see https://docs.docker.com/reference/cli/dockerd/
|
||||
#
|
||||
# remote-1: tcp://10.0.2.1:2375
|
||||
# remote-2: ssh://root:1234@10.0.2.2
|
||||
# if match_domains not defined
|
||||
# any host = alias+[any domain] will match
|
||||
# i.e. https://app1.y.z will match alias app1 for any domain y.z
|
||||
# but https://app1.node1.y.z will only match alias "app.node1"
|
||||
#
|
||||
# if match_domains defined
|
||||
# only host = alias+[one of match_domains] will match
|
||||
# i.e. match_domains = [node1.my.app, my.site]
|
||||
# https://app1.my.app, https://app1.my.net, etc. will not match even if app1 exists
|
||||
# only https://*.node1.my.app and https://*.my.site will match
|
||||
#
|
||||
#
|
||||
# match_domains:
|
||||
# - my.site
|
||||
# - node1.my.app
|
||||
|
||||
# Fixed options (optional, non hot-reloadable)
|
||||
# Below are fixed options (non hot-reloadable)
|
||||
|
||||
# timeout for shutdown (in seconds)
|
||||
#
|
||||
# timeout_shutdown: 5
|
||||
|
||||
# global setting redirect http requests to https (if https available, otherwise this will be ignored)
|
||||
# proxy.<alias>.middlewares.redirect_http will override this
|
||||
#
|
||||
# redirect_to_https: false
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
129
docs/docker.md
129
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:
|
||||
@@ -59,57 +36,61 @@
|
||||
autocert:
|
||||
email: john.doe@x.y.z # ACME Email
|
||||
domains: # a list of domains for cert registration
|
||||
- x.y.z
|
||||
- y.z
|
||||
- *.y.z
|
||||
provider: cloudflare
|
||||
options:
|
||||
- auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
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:
|
||||
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)
|
||||
|
||||
## Labels
|
||||
|
||||
**Parts surrounded by `[]` are optional**
|
||||
|
||||
### Syntax
|
||||
|
||||
| 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 |
|
||||
| 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 | | `30s` | `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 |
|
||||
| `proxy.?.middlewares.<middleware>[.<field>]` | enable and set field for specific middleware | **?** here means `<alias>` / `$<index>` / `*` <ul><li>`proxy.#1.middlewares.modify_request.set_headers`</li><li>`proxy.*.middlewares.modify_response.hide_headers`</li><li>`proxy.app1.middlewares.redirect_http`</li></ul> | N/A | Middleware specific<br>See [middlewares.md](middlewares.md) for more |
|
||||
|
||||
### 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 returned from docker | 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.<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 | empty **(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 |
|
||||
| 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)) |
|
||||
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -122,12 +103,9 @@ services:
|
||||
nginx:
|
||||
...
|
||||
labels:
|
||||
# values from duplicated header keys will be combined
|
||||
proxy.nginx.set_headers: | # remember to add the '|'
|
||||
proxy.nginx.middlewares.modify_request.set_headers: | # remember to add the '|'
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
X-Custom-Header2: value4
|
||||
# X-Custom-Header2 will be "value3, value4"
|
||||
X-Custom-Header2: value3, value4
|
||||
```
|
||||
|
||||
File Provider
|
||||
@@ -135,10 +113,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
|
||||
middlewares:
|
||||
modify_request:
|
||||
set_headers:
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
@@ -155,12 +134,12 @@ services:
|
||||
proxy.nginx.path_patterns: | # remember to add the '|'
|
||||
- GET /
|
||||
- POST /auth
|
||||
proxy.nginx.hide_headers: | # remember to add the '|'
|
||||
proxy.nginx.middlewares.modify_request.hide_headers: | # remember to add the '|'
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
```
|
||||
|
||||
File Provider
|
||||
Include file
|
||||
|
||||
```yaml
|
||||
service_a:
|
||||
@@ -168,9 +147,11 @@ service_a:
|
||||
path_patterns:
|
||||
- GET /
|
||||
- POST /auth
|
||||
hide_headers:
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
middlewares:
|
||||
modify_request:
|
||||
hide_headers:
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
@@ -230,10 +211,10 @@ services:
|
||||
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
|
||||
- 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
|
||||
@@ -266,8 +247,8 @@ services:
|
||||
labels:
|
||||
- proxy.aliases=pal1,pal2
|
||||
- proxy.*.scheme=udp
|
||||
- proxy.$1.port=20002:8211
|
||||
- proxy.$2.port=20003:27015
|
||||
- proxy.#1.port=20002:8211
|
||||
- proxy.#2.port=20003:27015
|
||||
environment: ...
|
||||
volumes:
|
||||
- palworld:/palworld
|
||||
|
||||
@@ -23,7 +23,7 @@ docker-proxy:
|
||||
ports:
|
||||
- 2375:2375
|
||||
# or more secure
|
||||
- <machine_ip>:2375:2375
|
||||
- <machine_private_ip>:2375:2375
|
||||
```
|
||||
|
||||
```yml
|
||||
|
||||
333
docs/middlewares.md
Normal file
333
docs/middlewares.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# Middlewares
|
||||
|
||||
## Table of content
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [Middlewares](#middlewares)
|
||||
- [Table of content](#table-of-content)
|
||||
- [Available middlewares](#available-middlewares)
|
||||
- [Redirect http](#redirect-http)
|
||||
- [Custom error pages](#custom-error-pages)
|
||||
- [Modify request or response](#modify-request-or-response)
|
||||
- [Set headers](#set-headers)
|
||||
- [Add headers](#add-headers)
|
||||
- [Hide headers](#hide-headers)
|
||||
- [X-Forwarded-\* Headers](#x-forwarded--headers)
|
||||
- [Add X-Forwarded-\*](#add-x-forwarded-)
|
||||
- [Set X-Forwarded-\*](#set-x-forwarded-)
|
||||
- [Forward Authorization header (experimental)](#forward-authorization-header-experimental)
|
||||
- [Examples](#examples)
|
||||
- [Authentik (untested, experimental)](#authentik-untested-experimental)
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
## Available middlewares
|
||||
|
||||
### Redirect http
|
||||
|
||||
Redirect http requests to https
|
||||
|
||||
```yaml
|
||||
# docker labels
|
||||
proxy.app1.middlewares.redirect_http:
|
||||
|
||||
# include file
|
||||
app1:
|
||||
middlewares:
|
||||
redirect_http:
|
||||
```
|
||||
|
||||
nginx equivalent:
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name domain.tld;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Custom error pages
|
||||
|
||||
To enable custom error pages, mount a folder containing your `html`, `js`, `css` files to `/app/error_pages` of _go-proxy_ container **(subfolders are ignored, please place all files in root directory)**
|
||||
|
||||
Any path under `error_pages` directory (e.g. `href` tag), should starts with `/$gperrorpage/`
|
||||
|
||||
Example:
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<title>404 Not Found</title>
|
||||
<link type="text/css" rel="stylesheet" href="/$gperrorpage/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
...
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Hot-reloading is **supported**, you can **edit**, **rename** or **delete** files **without restarting**. Changes will be reflected after page reload
|
||||
|
||||
Error page will be served if:
|
||||
|
||||
- route does not exist or domain does not match any of `match_domains`
|
||||
|
||||
or
|
||||
|
||||
- status code is not in range of 200 to 300, and
|
||||
- content type is `text/html`, `application/xhtml+xml` or `text/plain`
|
||||
|
||||
Error page will be served:
|
||||
|
||||
- from file `<statusCode>.html` if exists
|
||||
- otherwise from `404.html`
|
||||
- if they don't exist, original response will be served
|
||||
|
||||
```yaml
|
||||
# docker labels
|
||||
proxy.app1.middlewares.custom_error_page:
|
||||
|
||||
# include file
|
||||
app1:
|
||||
middlewares:
|
||||
custom_error_page:
|
||||
```
|
||||
|
||||
nginx equivalent:
|
||||
```nginx
|
||||
location / {
|
||||
try_files $uri $uri/ /error_pages/404.html =404;
|
||||
}
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Modify request or response
|
||||
|
||||
```yaml
|
||||
# docker labels
|
||||
proxy.app1.middlewares.modify_request.field:
|
||||
proxy.app1.middlewares.modify_response.field:
|
||||
|
||||
# include file
|
||||
app1:
|
||||
middlewares:
|
||||
modify_request:
|
||||
field:
|
||||
modify_response:
|
||||
field:
|
||||
```
|
||||
|
||||
#### Set headers
|
||||
|
||||
```yaml
|
||||
# docker labels
|
||||
proxy.app1.middlewares.modify_request.set_headers: |
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
|
||||
# include file
|
||||
app1:
|
||||
middlewares:
|
||||
modify_request:
|
||||
set_headers:
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
```
|
||||
|
||||
nginx equivalent:
|
||||
```nginx
|
||||
location / {
|
||||
add_header X-Custom-Header1 value1, value2;
|
||||
add_header X-Custom-Header2 value3;
|
||||
}
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
#### Add headers
|
||||
|
||||
```yaml
|
||||
# docker labels
|
||||
proxy.app1.middlewares.modify_request.add_headers: |
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
|
||||
# include file
|
||||
app1:
|
||||
middlewares:
|
||||
modify_request:
|
||||
add_headers:
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
```
|
||||
|
||||
nginx equivalent:
|
||||
```nginx
|
||||
location / {
|
||||
more_set_headers "X-Custom-Header1: value1, value2";
|
||||
more_set_headers "X-Custom-Header2: value3";
|
||||
}
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
#### Hide headers
|
||||
|
||||
```yaml
|
||||
# docker labels
|
||||
proxy.app1.middlewares.modify_request.hide_headers: |
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
|
||||
# include file
|
||||
app1:
|
||||
middlewares:
|
||||
modify_request:
|
||||
hide_headers:
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
```
|
||||
|
||||
nginx equivalent:
|
||||
```nginx
|
||||
location / {
|
||||
more_clear_headers "X-Custom-Header1";
|
||||
more_clear_headers "X-Custom-Header2";
|
||||
}
|
||||
```
|
||||
|
||||
### X-Forwarded-* Headers
|
||||
|
||||
#### Add X-Forwarded-*
|
||||
|
||||
Append `X-Forwarded-*` headers to existing headers
|
||||
|
||||
```yaml
|
||||
# docker labels
|
||||
proxy.app1.middlewares.modify_request.add_x_forwarded:
|
||||
|
||||
# include file
|
||||
app1:
|
||||
middlewares:
|
||||
modify_request:
|
||||
add_x_forwarded:
|
||||
```
|
||||
|
||||
#### Set X-Forwarded-*
|
||||
|
||||
Replace existing `X-Forwarded-*` headers with `go-proxy` provided headers
|
||||
|
||||
```yaml
|
||||
# docker labels
|
||||
proxy.app1.middlewares.modify_request.set_x_forwarded:
|
||||
|
||||
# include file
|
||||
app1:
|
||||
middlewares:
|
||||
modify_request:
|
||||
set_x_forwarded:
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Forward Authorization header (experimental)
|
||||
|
||||
Fields:
|
||||
- `address`: authentication provider URL _(required)_
|
||||
- `trust_forward_header`: whether to trust `X-Forwarded-*` headers from upstream proxies _(default: `false`)_
|
||||
- `auth_response_headers`: list of headers to copy from auth response _(default: empty)_
|
||||
- `add_auth_cookies_to_response`: list of cookies to add to response _(default: empty)_
|
||||
|
||||
```yaml
|
||||
# docker labels
|
||||
proxy.app1.middlewares.forward_auth.address: https://auth.example.com
|
||||
proxy.app1.middlewares.forward_auth.trust_forward_header: true
|
||||
proxy.app1.middlewares.forward_auth.auth_response_headers: |
|
||||
- X-Auth-Token
|
||||
- X-Auth-User
|
||||
proxy.app1.middlewares.forward_auth.add_auth_cookies_to_response: |
|
||||
- uid
|
||||
- session_id
|
||||
|
||||
# include file
|
||||
app1:
|
||||
middlewares:
|
||||
forward_authorization:
|
||||
address: https://auth.example.com
|
||||
trust_forward_header: true
|
||||
auth_response_headers:
|
||||
- X-Auth-Token
|
||||
- X-Auth-User
|
||||
add_auth_cookies_to_response:
|
||||
- uid
|
||||
- session_id
|
||||
```
|
||||
|
||||
Traefik equivalent:
|
||||
```yaml
|
||||
# docker labels
|
||||
traefik.http.middlewares.authentik.forwardauth.address: https://auth.example.com
|
||||
traefik.http.middlewares.authentik.forwardauth.trustForwardHeader: true
|
||||
traefik.http.middlewares.authentik.forwardauth.authResponseHeaders: X-Auth-Token, X-Auth-User
|
||||
traefik.http.middlewares.authentik.forwardauth.addAuthCookiesToResponse: uid, session_id
|
||||
|
||||
# standalone
|
||||
http:
|
||||
middlewares:
|
||||
forwardAuth:
|
||||
address: https://auth.example.com
|
||||
trustForwardHeader: true
|
||||
authResponseHeaders:
|
||||
- X-Auth-Token
|
||||
- X-Auth-User
|
||||
addAuthCookiesToResponse:
|
||||
- uid
|
||||
- session_id
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Examples
|
||||
|
||||
### Authentik (untested, experimental)
|
||||
|
||||
```yaml
|
||||
# docker compose
|
||||
services:
|
||||
...
|
||||
server:
|
||||
...
|
||||
container_name: authentik
|
||||
labels:
|
||||
proxy.authentik.middlewares.redirect_http:
|
||||
proxy.authentik.middlewares.set_x_forwarded:
|
||||
proxy.authentik.middlewares.modify_request.add_headers: |
|
||||
Strict-Transport-Security: "max-age=63072000" always
|
||||
|
||||
whoami:
|
||||
image: containous/whoami
|
||||
container_name: whoami
|
||||
ports:
|
||||
- 80
|
||||
labels:
|
||||
proxy.#1.middlewares.forward_auth.address: https://your_authentik_forward_address
|
||||
proxy.#1.middlewares.forward_auth.trustForwardHeader: true
|
||||
proxy.#1.middlewares.forward_auth.authResponseHeaders: |
|
||||
- X-authentik-username
|
||||
- X-authentik-groups
|
||||
- X-authentik-email
|
||||
- X-authentik-name
|
||||
- X-authentik-uid
|
||||
- X-authentik-jwt
|
||||
- X-authentik-meta-jwks
|
||||
- X-authentik-meta-outpost
|
||||
- X-authentik-meta-provider
|
||||
- X-authentik-meta-app
|
||||
- X-authentik-meta-version
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
1
frontend
1
frontend
Submodule frontend deleted from 441fd708db
@@ -1,6 +1,6 @@
|
||||
module github.com/yusing/go-proxy
|
||||
|
||||
go 1.22.0
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/docker/cli v27.3.1+incompatible
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.106.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
@@ -29,6 +29,7 @@ require (
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
@@ -37,11 +38,12 @@ require (
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
|
||||
go.opentelemetry.io/otel v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.30.0 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
@@ -4,10 +4,11 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cloudflare/cloudflare-go v0.104.0 h1:R/lB0dZupaZbOgibAH/BRrkFbZ6Acn/WsKg2iX2xXuY=
|
||||
github.com/cloudflare/cloudflare-go v0.104.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM=
|
||||
github.com/cloudflare/cloudflare-go v0.106.0 h1:q41gC5Wc1nfi0D1ZhSHokWcd9mGMbqC7RE7qiP+qE00=
|
||||
github.com/cloudflare/cloudflare-go v0.106.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -43,14 +44,16 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
|
||||
@@ -69,14 +72,16 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
|
||||
github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
|
||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
|
||||
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
@@ -91,18 +96,18 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI=
|
||||
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
|
||||
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8=
|
||||
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
|
||||
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
|
||||
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
|
||||
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
|
||||
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
|
||||
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@@ -148,14 +153,14 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.63.1 h1:pNClQmvdlyNUiwFETOux/PYqfhmA7BrswEdGRnib1fA=
|
||||
google.golang.org/grpc v1.63.1/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
|
||||
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
58
internal/api/handler.go
Normal file
58
internal/api/handler.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/error_page"
|
||||
. "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/config"
|
||||
)
|
||||
|
||||
type ServeMux struct{ *http.ServeMux }
|
||||
|
||||
func NewServeMux() ServeMux {
|
||||
return ServeMux{http.NewServeMux()}
|
||||
}
|
||||
|
||||
func (mux ServeMux) HandleFunc(method, endpoint string, handler http.HandlerFunc) {
|
||||
mux.ServeMux.HandleFunc(fmt.Sprintf("%s %s", method, endpoint), checkHost(handler))
|
||||
}
|
||||
|
||||
func NewHandler(cfg *config.Config) http.Handler {
|
||||
mux := NewServeMux()
|
||||
mux.HandleFunc("GET", "/v1", v1.Index)
|
||||
mux.HandleFunc("GET", "/v1/checkhealth", wrap(cfg, v1.CheckHealth))
|
||||
mux.HandleFunc("HEAD", "/v1/checkhealth", wrap(cfg, v1.CheckHealth))
|
||||
mux.HandleFunc("POST", "/v1/reload", wrap(cfg, v1.Reload))
|
||||
mux.HandleFunc("GET", "/v1/list", wrap(cfg, v1.List))
|
||||
mux.HandleFunc("GET", "/v1/list/{what}", wrap(cfg, v1.List))
|
||||
mux.HandleFunc("GET", "/v1/file", v1.GetFileContent)
|
||||
mux.HandleFunc("GET", "/v1/file/{filename}", v1.GetFileContent)
|
||||
mux.HandleFunc("POST", "/v1/file/{filename}", v1.SetFileContent)
|
||||
mux.HandleFunc("PUT", "/v1/file/{filename}", v1.SetFileContent)
|
||||
mux.HandleFunc("GET", "/v1/stats", wrap(cfg, v1.Stats))
|
||||
mux.HandleFunc("GET", "/v1/error_page", error_page.GetHandleFunc())
|
||||
return mux
|
||||
}
|
||||
|
||||
// allow only requests to API server with host matching common.APIHTTPAddr
|
||||
func checkHost(f http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Host != common.APIHTTPAddr {
|
||||
Logger.Warnf("invalid request to API server with host: %s, expect %s", r.Host, common.APIHTTPAddr)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("invalid request"))
|
||||
return
|
||||
}
|
||||
f(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func wrap(cfg *config.Config, f func(cfg *config.Config, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
f(cfg, w, r)
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
U "github.com/yusing/go-proxy/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/config"
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/config"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
)
|
||||
|
||||
func CheckHealth(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
88
internal/api/v1/error_page/error_page.go
Normal file
88
internal/api/v1/error_page/error_page.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package error_page
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
api "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
)
|
||||
|
||||
const errPagesBasePath = common.ErrorPagesBasePath
|
||||
|
||||
var setup = sync.OnceFunc(func() {
|
||||
dirWatcher = W.NewDirectoryWatcher(context.Background(), errPagesBasePath)
|
||||
loadContent()
|
||||
go watchDir()
|
||||
})
|
||||
|
||||
func GetStaticFile(filename string) ([]byte, bool) {
|
||||
return fileContentMap.Load(filename)
|
||||
}
|
||||
|
||||
// try <statusCode>.html -> 404.html -> not ok
|
||||
func GetErrorPageByStatus(statusCode int) (content []byte, ok bool) {
|
||||
content, ok = fileContentMap.Load(fmt.Sprintf("%d.html", statusCode))
|
||||
if !ok && statusCode != 404 {
|
||||
return fileContentMap.Load("404.html")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func loadContent() {
|
||||
files, err := U.ListFiles(errPagesBasePath, 0)
|
||||
if err != nil {
|
||||
api.Logger.Error(err)
|
||||
return
|
||||
}
|
||||
for _, file := range files {
|
||||
if fileContentMap.Has(file) {
|
||||
continue
|
||||
}
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
api.Logger.Errorf("failed to read error page resource %s: %s", file, err)
|
||||
continue
|
||||
}
|
||||
file = path.Base(file)
|
||||
api.Logger.Infof("error page resource %s loaded", file)
|
||||
fileContentMap.Store(file, content)
|
||||
}
|
||||
}
|
||||
|
||||
func watchDir() {
|
||||
eventCh, errCh := dirWatcher.Events(context.Background())
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-eventCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
filename := event.ActorName
|
||||
switch event.Action {
|
||||
case events.ActionFileWritten:
|
||||
fileContentMap.Delete(filename)
|
||||
loadContent()
|
||||
case events.ActionFileDeleted:
|
||||
fileContentMap.Delete(filename)
|
||||
api.Logger.Infof("error page resource %s deleted", filename)
|
||||
case events.ActionFileRenamed:
|
||||
api.Logger.Infof("error page resource %s deleted", filename)
|
||||
fileContentMap.Delete(filename)
|
||||
loadContent()
|
||||
}
|
||||
case err := <-errCh:
|
||||
api.Logger.Errorf("error watching error page directory: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dirWatcher W.Watcher
|
||||
var fileContentMap = F.NewMapOf[string, []byte]()
|
||||
25
internal/api/v1/error_page/http_handler.go
Normal file
25
internal/api/v1/error_page/http_handler.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package error_page
|
||||
|
||||
import "net/http"
|
||||
|
||||
func GetHandleFunc() http.HandlerFunc {
|
||||
setup()
|
||||
return serveHTTP
|
||||
}
|
||||
|
||||
func serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/" {
|
||||
http.Error(w, "invalid path", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
content, ok := fileContentMap.Load(r.URL.Path)
|
||||
if !ok {
|
||||
http.Error(w, "404 not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Write(content)
|
||||
}
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
U "github.com/yusing/go-proxy/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/common"
|
||||
"github.com/yusing/go-proxy/config"
|
||||
"github.com/yusing/go-proxy/proxy/provider"
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/config"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/proxy/provider"
|
||||
)
|
||||
|
||||
func GetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -37,14 +38,15 @@ func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var validateErr E.NestedError
|
||||
if filename == common.ConfigFileName {
|
||||
err = config.Validate(content).Error()
|
||||
validateErr = config.Validate(content)
|
||||
} else {
|
||||
err = provider.Validate(content).Error()
|
||||
validateErr = provider.Validate(content)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, err, http.StatusBadRequest)
|
||||
if validateErr != nil {
|
||||
U.RespondJson(w, validateErr.JSONObject(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/yusing/go-proxy/common"
|
||||
"github.com/yusing/go-proxy/config"
|
||||
|
||||
U "github.com/yusing/go-proxy/api/v1/utils"
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/config"
|
||||
)
|
||||
|
||||
func List(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -38,7 +37,7 @@ func listRoutes(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := U.RespondJson(routes, w); err != nil {
|
||||
if err := U.RespondJson(w, routes); err != nil {
|
||||
U.HandleErr(w, r, err)
|
||||
}
|
||||
}
|
||||
16
internal/api/v1/reload.go
Normal file
16
internal/api/v1/reload.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/config"
|
||||
)
|
||||
|
||||
func Reload(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
if err := cfg.Reload(); err != nil {
|
||||
U.RespondJson(w, err.JSONObject(), http.StatusInternalServerError)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@ package v1
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
U "github.com/yusing/go-proxy/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/config"
|
||||
"github.com/yusing/go-proxy/server"
|
||||
"github.com/yusing/go-proxy/utils"
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/config"
|
||||
"github.com/yusing/go-proxy/internal/server"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -14,7 +14,7 @@ func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
"proxies": cfg.Statistics(),
|
||||
"uptime": utils.FormatDuration(server.GetProxyServer().Uptime()),
|
||||
}
|
||||
if err := U.RespondJson(stats, w); err != nil {
|
||||
if err := U.RespondJson(w, stats); err != nil {
|
||||
U.HandleErr(w, r, err)
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,14 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
var Logger = logrus.WithField("module", "api")
|
||||
|
||||
func HandleErr(w http.ResponseWriter, r *http.Request, origErr error, code ...int) {
|
||||
err := E.From(origErr).Subjectf("%s %s", r.Method, r.URL)
|
||||
logrus.WithField("module", "api").Error(err)
|
||||
Logger.Error(err)
|
||||
if len(code) > 0 {
|
||||
http.Error(w, err.String(), code[0])
|
||||
return
|
||||
33
internal/api/v1/utils/health_check.go
Normal file
33
internal/api/v1/utils/health_check.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
)
|
||||
|
||||
func IsSiteHealthy(url string) bool {
|
||||
// try HEAD first
|
||||
// if HEAD is not allowed, try GET
|
||||
resp, err := httpClient.Head(url)
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
if err != nil && resp != nil && resp.StatusCode == http.StatusMethodNotAllowed {
|
||||
_, err = httpClient.Get(url)
|
||||
}
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func IsStreamHealthy(scheme, address string) bool {
|
||||
conn, err := net.DialTimeout(scheme, address, common.DialTimeout)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
conn.Close()
|
||||
return true
|
||||
}
|
||||
23
internal/api/v1/utils/http_client.go
Normal file
23
internal/api/v1/utils/http_client.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Timeout: common.ConnectionTimeout,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DisableKeepAlives: true,
|
||||
ForceAttemptHTTP2: true,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: common.DialTimeout,
|
||||
KeepAlive: common.KeepAlive, // this is different from DisableKeepAlives
|
||||
}).DialContext,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
31
internal/api/v1/utils/localhost.go
Normal file
31
internal/api/v1/utils/localhost.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
func ReloadServer() E.NestedError {
|
||||
resp, err := httpClient.Post(fmt.Sprintf("%s/v1/reload", common.APIHTTPURL), "", nil)
|
||||
if err != nil {
|
||||
return E.From(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
failure := E.Failure("server reload").Extraf("status code: %v", resp.StatusCode)
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return failure.Extraf("unable to read response body: %s", err)
|
||||
}
|
||||
reloadErr, ok := E.FromJSON(b)
|
||||
if ok {
|
||||
return E.Join("reload success, but server returned error", reloadErr)
|
||||
}
|
||||
return failure.Extraf("unable to read response body")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -5,9 +5,12 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func RespondJson(data any, w http.ResponseWriter) error {
|
||||
func RespondJson(w http.ResponseWriter, data any, code ...int) error {
|
||||
if len(code) > 0 {
|
||||
w.WriteHeader(code[0])
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
j, err := json.Marshal(data)
|
||||
j, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
M "github.com/yusing/go-proxy/internal/models"
|
||||
)
|
||||
|
||||
type Config M.AutoCertConfig
|
||||
@@ -32,13 +32,13 @@ func (cfg *Config) GetProvider() (provider *Provider, res E.NestedError) {
|
||||
|
||||
if cfg.Provider != ProviderLocal {
|
||||
if len(cfg.Domains) == 0 {
|
||||
b.Addf("no domains specified")
|
||||
b.Addf("%s", "no domains specified")
|
||||
}
|
||||
if cfg.Provider == "" {
|
||||
b.Addf("no provider specified")
|
||||
b.Addf("%s", "no provider specified")
|
||||
}
|
||||
if cfg.Email == "" {
|
||||
b.Addf("no email specified")
|
||||
b.Addf("%s", "no email specified")
|
||||
}
|
||||
// check if provider is implemented
|
||||
_, ok := providersGenMap[cfg.Provider]
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
@@ -13,9 +14,9 @@ import (
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
M "github.com/yusing/go-proxy/internal/models"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
@@ -59,8 +60,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).WithSeverity(E.SeverityWarning)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.client == nil {
|
||||
@@ -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,31 +187,23 @@ 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-------
|
||||
//* This should have been done in setup
|
||||
//* but double check is always a good choice
|
||||
_, err := os.Stat(path.Dir(p.cfg.CertPath))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(path.Dir(p.cfg.CertPath), 0o755); err != nil {
|
||||
return E.FailWith("create cert directory", err)
|
||||
}
|
||||
} else {
|
||||
return E.FailWith("stat cert directory", err)
|
||||
}
|
||||
}
|
||||
err = os.WriteFile(p.cfg.KeyPath, cert.PrivateKey, 0o600) // -rw-------
|
||||
if err != nil {
|
||||
return E.FailWith("write key file", err)
|
||||
}
|
||||
@@ -247,6 +239,10 @@ func (p *Provider) certState() CertState {
|
||||
}
|
||||
|
||||
func (p *Provider) renewIfNeeded() E.NestedError {
|
||||
if p.cfg.Provider == ProviderLocal {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch p.certState() {
|
||||
case CertStateExpired:
|
||||
logger.Info("certs expired, renewing")
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
. "github.com/yusing/go-proxy/utils/testing"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
func (p *Provider) Setup(ctx context.Context) (err E.NestedError) {
|
||||
@@ -14,7 +14,7 @@ func (p *Provider) Setup(ctx context.Context) (err E.NestedError) {
|
||||
}
|
||||
logger.Debug("obtaining cert due to error loading cert")
|
||||
if err = p.ObtainCert(); err != nil {
|
||||
return err.Warn()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
9
internal/autocert/state.go
Normal file
9
internal/autocert/state.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package autocert
|
||||
|
||||
type CertState int
|
||||
|
||||
const (
|
||||
CertStateValid CertState = iota
|
||||
CertStateExpired
|
||||
CertStateMismatch
|
||||
)
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"flag"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
@@ -13,6 +13,7 @@ type Args struct {
|
||||
|
||||
const (
|
||||
CommandStart = ""
|
||||
CommandSetup = "setup"
|
||||
CommandValidate = "validate"
|
||||
CommandListConfigs = "ls-config"
|
||||
CommandListRoutes = "ls-routes"
|
||||
@@ -23,6 +24,7 @@ const (
|
||||
|
||||
var ValidCommands = []string{
|
||||
CommandStart,
|
||||
CommandSetup,
|
||||
CommandValidate,
|
||||
CommandListConfigs,
|
||||
CommandListRoutes,
|
||||
52
internal/common/constants.go
Normal file
52
internal/common/constants.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ConnectionTimeout = 5 * time.Second
|
||||
DialTimeout = 3 * time.Second
|
||||
KeepAlive = 60 * time.Second
|
||||
)
|
||||
|
||||
// file, folder structure
|
||||
|
||||
const (
|
||||
ConfigBasePath = "config"
|
||||
ConfigFileName = "config.yml"
|
||||
ConfigExampleFileName = "config.example.yml"
|
||||
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
||||
)
|
||||
|
||||
const (
|
||||
SchemaBasePath = "schema"
|
||||
ConfigSchemaPath = SchemaBasePath + "/config.schema.json"
|
||||
FileProviderSchemaPath = SchemaBasePath + "/providers.schema.json"
|
||||
)
|
||||
|
||||
const (
|
||||
ComposeFileName = "compose.yml"
|
||||
ComposeExampleFileName = "compose.example.yml"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrorPagesBasePath = "error_pages"
|
||||
)
|
||||
|
||||
var (
|
||||
RequiredDirectories = []string{
|
||||
ConfigBasePath,
|
||||
SchemaBasePath,
|
||||
ErrorPagesBasePath,
|
||||
}
|
||||
)
|
||||
|
||||
const DockerHostFromEnv = "$DOCKER_HOST"
|
||||
|
||||
const (
|
||||
IdleTimeoutDefault = "0"
|
||||
WakeTimeoutDefault = "30s"
|
||||
StopTimeoutDefault = "10s"
|
||||
StopMethodDefault = "stop"
|
||||
)
|
||||
59
internal/common/env.go
Normal file
59
internal/common/env.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
NoSchemaValidation = GetEnvBool("GOPROXY_NO_SCHEMA_VALIDATION", false)
|
||||
IsDebug = GetEnvBool("GOPROXY_DEBUG", false)
|
||||
|
||||
ProxyHTTPAddr,
|
||||
ProxyHTTPHost,
|
||||
ProxyHTTPPort,
|
||||
ProxyHTTPURL = GetAddrEnv("GOPROXY_HTTP_ADDR", ":80", "http")
|
||||
|
||||
ProxyHTTPSAddr,
|
||||
ProxyHTTPSHost,
|
||||
ProxyHTTPSPort,
|
||||
ProxyHTTPSURL = GetAddrEnv("GOPROXY_HTTPS_ADDR", ":443", "https")
|
||||
|
||||
APIHTTPAddr,
|
||||
APIHTTPHost,
|
||||
APIHTTPPort,
|
||||
APIHTTPURL = GetAddrEnv("GOPROXY_API_ADDR", "127.0.0.1:8888", "http")
|
||||
)
|
||||
|
||||
func GetEnvBool(key string, defaultValue bool) bool {
|
||||
value, ok := os.LookupEnv(key)
|
||||
if !ok || value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return U.ParseBool(value)
|
||||
}
|
||||
|
||||
func GetEnv(key, defaultValue string) string {
|
||||
value, ok := os.LookupEnv(key)
|
||||
if !ok || value == "" {
|
||||
value = defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func GetAddrEnv(key, defaultValue, scheme string) (addr, host, port, fullURL string) {
|
||||
addr = GetEnv(key, defaultValue)
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Invalid address: %s", addr)
|
||||
}
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
fullURL = fmt.Sprintf("%s://%s:%s", scheme, host, port)
|
||||
return
|
||||
}
|
||||
27
internal/common/http.go
Normal file
27
internal/common/http.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultDialer = net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
}
|
||||
DefaultTransport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: defaultDialer.DialContext,
|
||||
MaxIdleConnsPerHost: 1000,
|
||||
}
|
||||
DefaultTransportNoTLS = func() *http.Transport {
|
||||
var clone = DefaultTransport.Clone()
|
||||
clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
return clone
|
||||
}()
|
||||
)
|
||||
|
||||
const StaticFilePathPrefix = "/$gperrorpage/"
|
||||
@@ -10,14 +10,15 @@ var (
|
||||
}
|
||||
|
||||
ServiceNamePortMapTCP = map[string]int{
|
||||
"mssql": 1433,
|
||||
"mysql": 3306,
|
||||
"mariadb": 3306,
|
||||
"postgres": 5432,
|
||||
"rabbitmq": 5672,
|
||||
"redis": 6379,
|
||||
"memcached": 11211,
|
||||
"mongo": 27017,
|
||||
"mssql": 1433,
|
||||
"mysql": 3306,
|
||||
"mariadb": 3306,
|
||||
"postgres": 5432,
|
||||
"rabbitmq": 5672,
|
||||
"redis": 6379,
|
||||
"memcached": 11211,
|
||||
"mongo": 27017,
|
||||
"minecraft-server": 25565,
|
||||
|
||||
"ssh": 22,
|
||||
"ftp": 21,
|
||||
@@ -53,13 +54,13 @@ var (
|
||||
"immich": 3001,
|
||||
"jellyfin": 8096,
|
||||
"lidarr": 8686,
|
||||
"minecraft-server": 25565,
|
||||
"microbin": 8080,
|
||||
"nginx": 80,
|
||||
"nginx-proxy-manager": 81,
|
||||
"open-webui": 8080,
|
||||
"plex": 32400,
|
||||
"portainer": 9000,
|
||||
"portainer-ce": 9000,
|
||||
"portainer-be": 9443,
|
||||
"portainer-ce": 9443,
|
||||
"prometheus": 9090,
|
||||
"prowlarr": 9696,
|
||||
"radarr": 7878,
|
||||
@@ -5,16 +5,16 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/autocert"
|
||||
"github.com/yusing/go-proxy/common"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
PR "github.com/yusing/go-proxy/proxy/provider"
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
"github.com/yusing/go-proxy/watcher/events"
|
||||
"github.com/yusing/go-proxy/internal/autocert"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
M "github.com/yusing/go-proxy/internal/models"
|
||||
PR "github.com/yusing/go-proxy/internal/proxy/provider"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -31,25 +31,48 @@ type Config struct {
|
||||
reloadReq chan struct{}
|
||||
}
|
||||
|
||||
func Load() (*Config, E.NestedError) {
|
||||
cfg := &Config{
|
||||
var instance *Config
|
||||
|
||||
func GetInstance() *Config {
|
||||
return instance
|
||||
}
|
||||
|
||||
func Load() E.NestedError {
|
||||
if instance != nil {
|
||||
return nil
|
||||
}
|
||||
instance = &Config{
|
||||
value: M.DefaultConfig(),
|
||||
proxyProviders: F.NewMapOf[string, *PR.Provider](),
|
||||
l: logrus.WithField("module", "config"),
|
||||
watcher: W.NewFileWatcher(common.ConfigFileName),
|
||||
watcher: W.NewConfigFileWatcher(common.ConfigFileName),
|
||||
reloadReq: make(chan struct{}, 1),
|
||||
}
|
||||
return cfg, cfg.load()
|
||||
return instance.load()
|
||||
}
|
||||
|
||||
func Validate(data []byte) E.NestedError {
|
||||
return U.ValidateYaml(U.GetSchema(common.ConfigSchemaPath), data)
|
||||
}
|
||||
|
||||
func MatchDomains() []string {
|
||||
if instance == nil {
|
||||
logrus.Panic("config has not been loaded, please check if there is any errors")
|
||||
}
|
||||
return instance.value.MatchDomains
|
||||
}
|
||||
|
||||
func (cfg *Config) Value() M.Config {
|
||||
if cfg == nil {
|
||||
logrus.Panic("config has not been loaded, please check if there is any errors")
|
||||
}
|
||||
return *cfg.value
|
||||
}
|
||||
|
||||
func (cfg *Config) GetAutoCertProvider() *autocert.Provider {
|
||||
if instance == nil {
|
||||
logrus.Panic("config has not been loaded, please check if there is any errors")
|
||||
}
|
||||
return cfg.autocertProvider
|
||||
}
|
||||
|
||||
@@ -61,13 +84,11 @@ func (cfg *Config) Dispose() {
|
||||
cfg.stopProviders()
|
||||
}
|
||||
|
||||
func (cfg *Config) Reload() E.NestedError {
|
||||
func (cfg *Config) Reload() (err E.NestedError) {
|
||||
cfg.stopProviders()
|
||||
if err := cfg.load(); err.HasError() {
|
||||
return err
|
||||
}
|
||||
err = cfg.load()
|
||||
cfg.StartProxyProviders()
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
func (cfg *Config) StartProxyProviders() {
|
||||
@@ -95,8 +116,9 @@ func (cfg *Config) WatchChanges() {
|
||||
case <-cfg.watcherCtx.Done():
|
||||
return
|
||||
case event := <-eventCh:
|
||||
if event.Action == events.ActionFileDeleted {
|
||||
cfg.stopProviders()
|
||||
if event.Action == events.ActionFileDeleted || event.Action == events.ActionFileRenamed {
|
||||
cfg.l.Error("config file deleted or renamed, ignoring...")
|
||||
continue
|
||||
} else {
|
||||
cfg.reloadReq <- struct{}{}
|
||||
}
|
||||
@@ -126,28 +148,28 @@ func (cfg *Config) load() (res E.NestedError) {
|
||||
data, err := E.Check(os.ReadFile(common.ConfigPath))
|
||||
if err.HasError() {
|
||||
b.Add(E.FailWith("read config", err))
|
||||
return
|
||||
logrus.Fatal(b.Build())
|
||||
}
|
||||
|
||||
if !common.NoSchemaValidation {
|
||||
if err = Validate(data); err.HasError() {
|
||||
b.Add(E.FailWith("schema validation", err))
|
||||
return
|
||||
logrus.Fatal(b.Build())
|
||||
}
|
||||
}
|
||||
|
||||
model := M.DefaultConfig()
|
||||
if err := E.From(yaml.Unmarshal(data, model)); err.HasError() {
|
||||
b.Add(E.FailWith("parse config", err))
|
||||
return
|
||||
logrus.Fatal(b.Build())
|
||||
}
|
||||
|
||||
// errors are non fatal below
|
||||
b.WithSeverity(E.SeverityWarning)
|
||||
b.Add(cfg.initAutoCert(&model.AutoCert))
|
||||
b.Add(cfg.loadProviders(&model.Providers))
|
||||
|
||||
cfg.value = model
|
||||
R.SetFindMuxDomains(model.MatchDomains)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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"
|
||||
M "github.com/yusing/go-proxy/internal/models"
|
||||
PR "github.com/yusing/go-proxy/internal/proxy/provider"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
)
|
||||
|
||||
func (cfg *Config) DumpEntries() map[string]*M.RawEntry {
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/common"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type ClientInfo struct {
|
||||
@@ -6,36 +6,17 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type ProxyProperties struct {
|
||||
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 {
|
||||
*types.Container
|
||||
*ProxyProperties
|
||||
}
|
||||
|
||||
type PortMapping = map[string]types.Port
|
||||
|
||||
func FromDocker(c *types.Container, dockerHost string) (res Container) {
|
||||
res.Container = c
|
||||
isExplicit := c.Labels[LabelAliases] != ""
|
||||
res.ProxyProperties = &ProxyProperties{
|
||||
DockerHost: dockerHost,
|
||||
ContainerName: res.getName(),
|
||||
@@ -45,6 +26,7 @@ func FromDocker(c *types.Container, dockerHost string) (res Container) {
|
||||
NetworkMode: c.HostConfig.NetworkMode,
|
||||
Aliases: res.getAliases(),
|
||||
IsExcluded: U.ParseBool(res.getDeleteLabel(LabelExclude)),
|
||||
IsExplicit: isExplicit,
|
||||
IdleTimeout: res.getDeleteLabel(LabelIdleTimeout),
|
||||
WakeTimeout: res.getDeleteLabel(LabelWakeTimeout),
|
||||
StopMethod: res.getDeleteLabel(LabelStopMethod),
|
||||
@@ -59,8 +41,8 @@ func FromJson(json types.ContainerJSON, dockerHost string) Container {
|
||||
ports := make([]types.Port, 0)
|
||||
for k, bindings := range json.NetworkSettings.Ports {
|
||||
for _, v := range bindings {
|
||||
pubPort, _ := strconv.Atoi(v.HostPort)
|
||||
privPort, _ := strconv.Atoi(k.Port())
|
||||
pubPort, _ := strconv.ParseUint(v.HostPort, 10, 16)
|
||||
privPort, _ := strconv.ParseUint(k.Port(), 10, 16)
|
||||
ports = append(ports, types.Port{
|
||||
IP: v.HostIP,
|
||||
PublicPort: uint16(pubPort),
|
||||
@@ -68,7 +50,7 @@ func FromJson(json types.ContainerJSON, dockerHost string) Container {
|
||||
})
|
||||
}
|
||||
}
|
||||
return FromDocker(&types.Container{
|
||||
cont := FromDocker(&types.Container{
|
||||
ID: json.ID,
|
||||
Names: []string{json.Name},
|
||||
Image: json.Image,
|
||||
@@ -77,6 +59,8 @@ func FromJson(json types.ContainerJSON, dockerHost string) Container {
|
||||
State: json.State.Status,
|
||||
Status: json.State.Status,
|
||||
}, dockerHost)
|
||||
cont.NetworkMode = string(json.HostConfig.NetworkMode)
|
||||
return cont
|
||||
}
|
||||
|
||||
func (c Container) getDeleteLabel(label string) string {
|
||||
@@ -119,9 +103,6 @@ func (c Container) getPublicPortMapping() PortMapping {
|
||||
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
|
||||
@@ -19,13 +19,7 @@ type templateData struct {
|
||||
|
||||
//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
|
||||
}()
|
||||
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
|
||||
|
||||
const (
|
||||
htmlContentType = "text/html; charset=utf-8"
|
||||
@@ -3,7 +3,6 @@ package idlewatcher
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -18,14 +17,17 @@ func (rt roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
}
|
||||
|
||||
func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*http.Response, error) {
|
||||
// wake the container
|
||||
select {
|
||||
case w.wakeCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
@@ -57,7 +59,6 @@ func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*ht
|
||||
rtDone <- resp
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -66,6 +67,10 @@ func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*ht
|
||||
select {
|
||||
case resp := <-rtDone:
|
||||
return w.makeSuccResp(targetUrl, resp)
|
||||
case err := <-w.wakeDone:
|
||||
if err != nil {
|
||||
return w.makeErrResp("error waking up %s\n%s", w.ContainerName, err.Error())
|
||||
}
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return w.makeErrResp("Timed out waiting for %s to fully wake", w.ContainerName)
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/sirupsen/logrus"
|
||||
D "github.com/yusing/go-proxy/docker"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
P "github.com/yusing/go-proxy/proxy"
|
||||
PT "github.com/yusing/go-proxy/proxy/fields"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
P "github.com/yusing/go-proxy/internal/proxy"
|
||||
PT "github.com/yusing/go-proxy/internal/proxy/fields"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -78,8 +78,8 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||
ReverseProxyEntry: entry,
|
||||
client: client,
|
||||
refCount: &sync.WaitGroup{},
|
||||
wakeCh: make(chan struct{}, 1),
|
||||
wakeDone: make(chan E.NestedError, 1),
|
||||
wakeCh: make(chan struct{}),
|
||||
wakeDone: make(chan E.NestedError),
|
||||
l: logger.WithField("container", entry.ContainerName),
|
||||
}
|
||||
w.refCount.Add(1)
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
func (c Client) Inspect(containerID string) (Container, E.NestedError) {
|
||||
151
internal/docker/label.go
Normal file
151
internal/docker/label.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
)
|
||||
|
||||
/*
|
||||
Formats:
|
||||
- namespace.attribute
|
||||
- namespace.target.attribute
|
||||
- namespace.target.attribute.namespace2.attribute
|
||||
*/
|
||||
type (
|
||||
Label struct {
|
||||
Namespace string
|
||||
Target string
|
||||
Attribute string
|
||||
Value any
|
||||
}
|
||||
NestedLabelMap map[string]U.SerializedObject
|
||||
ValueParser func(string) (any, E.NestedError)
|
||||
ValueParserMap map[string]ValueParser
|
||||
)
|
||||
|
||||
func (l *Label) String() string {
|
||||
if l.Attribute == "" {
|
||||
return l.Namespace + "." + l.Target
|
||||
}
|
||||
return l.Namespace + "." + l.Target + "." + l.Attribute
|
||||
}
|
||||
|
||||
// Apply applies the value of a Label to the corresponding field in the given object.
|
||||
//
|
||||
// Parameters:
|
||||
// - obj: a pointer to the object to which the Label value will be applied.
|
||||
// - l: a pointer to the Label containing the attribute and value to be applied.
|
||||
//
|
||||
// Returns:
|
||||
// - error: an error if the field does not exist.
|
||||
func ApplyLabel[T any](obj *T, l *Label) E.NestedError {
|
||||
if obj == nil {
|
||||
return E.Invalid("nil object", l)
|
||||
}
|
||||
switch nestedLabel := l.Value.(type) {
|
||||
case *Label:
|
||||
var field reflect.Value
|
||||
objType := reflect.TypeFor[T]()
|
||||
for i := 0; i < reflect.TypeFor[T]().NumField(); i++ {
|
||||
if objType.Field(i).Tag.Get("yaml") == l.Attribute {
|
||||
field = reflect.ValueOf(obj).Elem().Field(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !field.IsValid() {
|
||||
return E.NotExist("field", l.Attribute)
|
||||
}
|
||||
dst, ok := field.Interface().(NestedLabelMap)
|
||||
if !ok {
|
||||
return E.Invalid("type", field.Type())
|
||||
}
|
||||
if dst == nil {
|
||||
field.Set(reflect.MakeMap(reflect.TypeFor[NestedLabelMap]()))
|
||||
dst = field.Interface().(NestedLabelMap)
|
||||
}
|
||||
if dst[nestedLabel.Namespace] == nil {
|
||||
dst[nestedLabel.Namespace] = make(U.SerializedObject)
|
||||
}
|
||||
dst[nestedLabel.Namespace][nestedLabel.Attribute] = nestedLabel.Value
|
||||
return nil
|
||||
default:
|
||||
return U.Deserialize(U.SerializedObject{l.Attribute: l.Value}, obj)
|
||||
}
|
||||
}
|
||||
|
||||
func ParseLabel(label string, value string) (*Label, E.NestedError) {
|
||||
parts := strings.Split(label, ".")
|
||||
|
||||
if len(parts) < 2 {
|
||||
return &Label{
|
||||
Namespace: label,
|
||||
Value: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
Namespace: parts[0],
|
||||
Target: parts[1],
|
||||
Value: value,
|
||||
}
|
||||
|
||||
switch len(parts) {
|
||||
case 2:
|
||||
l.Attribute = l.Target
|
||||
case 3:
|
||||
l.Attribute = parts[2]
|
||||
default:
|
||||
l.Attribute = parts[2]
|
||||
nestedLabel, err := ParseLabel(strings.Join(parts[3:], "."), value)
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
l.Value = nestedLabel
|
||||
}
|
||||
|
||||
// find if namespace has value parser
|
||||
pm, ok := valueParserMap.Load(U.ToLowerNoSnake(l.Namespace))
|
||||
if !ok {
|
||||
return l, nil
|
||||
}
|
||||
// find if attribute has value parser
|
||||
p, ok := pm[U.ToLowerNoSnake(l.Attribute)]
|
||||
if !ok {
|
||||
return l, nil
|
||||
}
|
||||
// try to parse value
|
||||
v, err := p(value)
|
||||
if err.HasError() {
|
||||
return nil, err.Subject(label)
|
||||
}
|
||||
l.Value = v
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func RegisterNamespace(namespace string, pm ValueParserMap) {
|
||||
pmCleaned := make(ValueParserMap, len(pm))
|
||||
for k, v := range pm {
|
||||
pmCleaned[U.ToLowerNoSnake(k)] = v
|
||||
}
|
||||
valueParserMap.Store(U.ToLowerNoSnake(namespace), pmCleaned)
|
||||
}
|
||||
|
||||
func GetRegisteredNamespaces() map[string][]string {
|
||||
r := make(map[string][]string)
|
||||
|
||||
valueParserMap.RangeAll(func(ns string, vpm ValueParserMap) {
|
||||
r[ns] = make([]string, 0, len(vpm))
|
||||
for attr := range vpm {
|
||||
r[ns] = append(r[ns], attr)
|
||||
}
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// namespace:target.attribute -> func(string) (any, error)
|
||||
var valueParserMap = F.NewMapOf[string, ValueParserMap]()
|
||||
78
internal/docker/label_parser.go
Normal file
78
internal/docker/label_parser.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
NSProxy = "proxy"
|
||||
ProxyAttributePathPatterns = "path_patterns"
|
||||
ProxyAttributeNoTLSVerify = "no_tls_verify"
|
||||
ProxyAttributeMiddlewares = "middlewares"
|
||||
)
|
||||
|
||||
var _ = func() int {
|
||||
RegisterNamespace(NSProxy, ValueParserMap{
|
||||
ProxyAttributePathPatterns: YamlStringListParser,
|
||||
ProxyAttributeNoTLSVerify: BoolParser,
|
||||
})
|
||||
return 0
|
||||
}()
|
||||
|
||||
func YamlStringListParser(value string) (any, E.NestedError) {
|
||||
/*
|
||||
- foo
|
||||
- bar
|
||||
- baz
|
||||
*/
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
var data []string
|
||||
err := E.From(yaml.Unmarshal([]byte(value), &data))
|
||||
return data, err
|
||||
}
|
||||
|
||||
func YamlLikeMappingParser(allowDuplicate bool) func(string) (any, E.NestedError) {
|
||||
return func(value string) (any, E.NestedError) {
|
||||
/*
|
||||
foo: bar
|
||||
boo: baz
|
||||
*/
|
||||
value = strings.TrimSpace(value)
|
||||
lines := strings.Split(value, "\n")
|
||||
h := make(map[string]string)
|
||||
for _, line := range lines {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, E.Invalid("syntax", line).With("too many colons")
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
val := strings.TrimSpace(parts[1])
|
||||
if existing, ok := h[key]; ok {
|
||||
if !allowDuplicate {
|
||||
return nil, E.Duplicated("key", key)
|
||||
}
|
||||
h[key] = existing + ", " + val
|
||||
} else {
|
||||
h[key] = val
|
||||
}
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
}
|
||||
|
||||
func BoolParser(value string) (any, E.NestedError) {
|
||||
switch strings.ToLower(value) {
|
||||
case "true", "yes", "1":
|
||||
return true, nil
|
||||
case "false", "no", "0":
|
||||
return false, nil
|
||||
default:
|
||||
return nil, E.Invalid("boolean value", value)
|
||||
}
|
||||
}
|
||||
106
internal/docker/label_parser_test.go
Normal file
106
internal/docker/label_parser_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func makeLabel(namespace string, alias string, field string) string {
|
||||
return fmt.Sprintf("%s.%s.%s", namespace, alias, field)
|
||||
}
|
||||
|
||||
func TestParseLabel(t *testing.T) {
|
||||
alias := "foo"
|
||||
field := "ip"
|
||||
v := "bar"
|
||||
pl, err := ParseLabel(makeLabel(NSHomePage, alias, field), v)
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectEqual(t, pl.Namespace, NSHomePage)
|
||||
ExpectEqual(t, pl.Target, alias)
|
||||
ExpectEqual(t, pl.Attribute, field)
|
||||
ExpectEqual(t, pl.Value.(string), v)
|
||||
}
|
||||
|
||||
func TestStringProxyLabel(t *testing.T) {
|
||||
v := "bar"
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "ip"), v)
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectEqual(t, pl.Value.(string), v)
|
||||
}
|
||||
|
||||
func TestBoolProxyLabelValid(t *testing.T) {
|
||||
tests := map[string]bool{
|
||||
"true": true,
|
||||
"TRUE": true,
|
||||
"yes": true,
|
||||
"1": true,
|
||||
"false": false,
|
||||
"FALSE": false,
|
||||
"no": false,
|
||||
"0": false,
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", ProxyAttributeNoTLSVerify), k)
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectEqual(t, pl.Value.(bool), v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoolProxyLabelInvalid(t *testing.T) {
|
||||
_, err := ParseLabel(makeLabel(NSProxy, "foo", ProxyAttributeNoTLSVerify), "invalid")
|
||||
if !err.Is(E.ErrInvalid) {
|
||||
t.Errorf("Expected err InvalidProxyLabel, got %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// func TestSetHeaderProxyLabelValid(t *testing.T) {
|
||||
// v := `
|
||||
// X-Custom-Header1: foo, bar
|
||||
// X-Custom-Header1: baz
|
||||
// X-Custom-Header2: boo`
|
||||
// v = strings.TrimPrefix(v, "\n")
|
||||
// h := map[string]string{
|
||||
// "X-Custom-Header1": "foo, bar, baz",
|
||||
// "X-Custom-Header2": "boo",
|
||||
// }
|
||||
|
||||
// pl, err := ParseLabel(makeLabel(NSProxy, "foo", ProxyAttributeSetHeaders), v)
|
||||
// ExpectNoError(t, err.Error())
|
||||
// hGot := ExpectType[map[string]string](t, pl.Value)
|
||||
// ExpectFalse(t, hGot == nil)
|
||||
// ExpectDeepEqual(t, h, hGot)
|
||||
// }
|
||||
|
||||
// func TestSetHeaderProxyLabelInvalid(t *testing.T) {
|
||||
// tests := []string{
|
||||
// "X-Custom-Header1 = bar",
|
||||
// "X-Custom-Header1",
|
||||
// "- X-Custom-Header1",
|
||||
// }
|
||||
|
||||
// for _, v := range tests {
|
||||
// _, err := ParseLabel(makeLabel(NSProxy, "foo", ProxyAttributeSetHeaders), v)
|
||||
// if !err.Is(E.ErrInvalid) {
|
||||
// t.Errorf("Expected invalid err for %q, got %s", v, err.Error())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestHideHeadersProxyLabel(t *testing.T) {
|
||||
// v := `
|
||||
// - X-Custom-Header1
|
||||
// - X-Custom-Header2
|
||||
// - X-Custom-Header3
|
||||
// `
|
||||
// v = strings.TrimPrefix(v, "\n")
|
||||
// pl, err := ParseLabel(makeLabel(NSProxy, "foo", ProxyAttributeHideHeaders), v)
|
||||
// ExpectNoError(t, err.Error())
|
||||
// sGot := ExpectType[[]string](t, pl.Value)
|
||||
// sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
|
||||
// ExpectFalse(t, sGot == nil)
|
||||
// ExpectDeepEqual(t, sGot, sWant)
|
||||
// }
|
||||
85
internal/docker/label_test.go
Normal file
85
internal/docker/label_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestNestedLabel(t *testing.T) {
|
||||
mName := "middleware1"
|
||||
mAttr := "prop1"
|
||||
v := "value1"
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s.%s", ProxyAttributeMiddlewares, mName, mAttr)), v)
|
||||
ExpectNoError(t, err.Error())
|
||||
sGot := ExpectType[*Label](t, pl.Value)
|
||||
ExpectFalse(t, sGot == nil)
|
||||
ExpectEqual(t, sGot.Namespace, mName)
|
||||
ExpectEqual(t, sGot.Attribute, mAttr)
|
||||
}
|
||||
|
||||
func TestApplyNestedLabel(t *testing.T) {
|
||||
entry := new(struct {
|
||||
Middlewares NestedLabelMap `yaml:"middlewares"`
|
||||
})
|
||||
mName := "middleware1"
|
||||
mAttr := "prop1"
|
||||
v := "value1"
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s.%s", ProxyAttributeMiddlewares, mName, mAttr)), v)
|
||||
ExpectNoError(t, err.Error())
|
||||
err = ApplyLabel(entry, pl)
|
||||
ExpectNoError(t, err.Error())
|
||||
middleware1, ok := entry.Middlewares[mName]
|
||||
ExpectTrue(t, ok)
|
||||
got := ExpectType[string](t, middleware1[mAttr])
|
||||
ExpectEqual(t, got, v)
|
||||
}
|
||||
|
||||
func TestApplyNestedLabelExisting(t *testing.T) {
|
||||
mName := "middleware1"
|
||||
mAttr := "prop1"
|
||||
v := "value1"
|
||||
|
||||
checkAttr := "prop2"
|
||||
checkV := "value2"
|
||||
entry := new(struct {
|
||||
Middlewares NestedLabelMap `yaml:"middlewares"`
|
||||
})
|
||||
entry.Middlewares = make(NestedLabelMap)
|
||||
entry.Middlewares[mName] = make(U.SerializedObject)
|
||||
entry.Middlewares[mName][checkAttr] = checkV
|
||||
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s.%s", ProxyAttributeMiddlewares, mName, mAttr)), v)
|
||||
ExpectNoError(t, err.Error())
|
||||
err = ApplyLabel(entry, pl)
|
||||
ExpectNoError(t, err.Error())
|
||||
middleware1, ok := entry.Middlewares[mName]
|
||||
ExpectTrue(t, ok)
|
||||
got := ExpectType[string](t, middleware1[mAttr])
|
||||
ExpectEqual(t, got, v)
|
||||
|
||||
// check if prop2 is affected
|
||||
ExpectFalse(t, middleware1[checkAttr] == nil)
|
||||
got = ExpectType[string](t, middleware1[checkAttr])
|
||||
ExpectEqual(t, got, checkV)
|
||||
}
|
||||
|
||||
func TestApplyNestedLabelNoAttr(t *testing.T) {
|
||||
mName := "middleware1"
|
||||
v := "value1"
|
||||
|
||||
entry := new(struct {
|
||||
Middlewares NestedLabelMap `yaml:"middlewares"`
|
||||
})
|
||||
entry.Middlewares = make(NestedLabelMap)
|
||||
entry.Middlewares[mName] = make(U.SerializedObject)
|
||||
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s", ProxyAttributeMiddlewares, mName)), v)
|
||||
ExpectNoError(t, err.Error())
|
||||
err = ApplyLabel(entry, pl)
|
||||
ExpectNoError(t, err.Error())
|
||||
_, ok := entry.Middlewares[mName]
|
||||
ExpectTrue(t, ok)
|
||||
}
|
||||
23
internal/docker/proxy_properties.go
Normal file
23
internal/docker/proxy_properties.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package docker
|
||||
|
||||
import "github.com/docker/docker/api/types"
|
||||
|
||||
type PortMapping = map[string]types.Port
|
||||
type ProxyProperties struct {
|
||||
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"`
|
||||
IsExplicit bool `yaml:"-" json:"is_explicit"`
|
||||
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"`
|
||||
}
|
||||
@@ -10,9 +10,8 @@ type Builder struct {
|
||||
}
|
||||
|
||||
type builder struct {
|
||||
message string
|
||||
errors []NestedError
|
||||
severity Severity
|
||||
message string
|
||||
errors []NestedError
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
@@ -40,11 +39,6 @@ func (b Builder) Addf(format string, args ...any) Builder {
|
||||
return b.Add(errorf(format, args...))
|
||||
}
|
||||
|
||||
func (b Builder) WithSeverity(s Severity) Builder {
|
||||
b.severity = s
|
||||
return b
|
||||
}
|
||||
|
||||
// Build builds a NestedError based on the errors collected in the Builder.
|
||||
//
|
||||
// If there are no errors in the Builder, it returns a Nil() NestedError.
|
||||
@@ -58,7 +52,7 @@ func (b Builder) Build() NestedError {
|
||||
} else if len(b.errors) == 1 {
|
||||
return b.errors[0]
|
||||
}
|
||||
return Join(b.message, b.errors...).Severity(b.severity)
|
||||
return Join(b.message, b.errors...)
|
||||
}
|
||||
|
||||
func (b Builder) To(ptr *NestedError) {
|
||||
@@ -3,7 +3,7 @@ package error
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/utils/testing"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestBuilderEmpty(t *testing.T) {
|
||||
@@ -1,6 +1,7 @@
|
||||
package error
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -9,17 +10,15 @@ import (
|
||||
type (
|
||||
NestedError = *nestedError
|
||||
nestedError struct {
|
||||
subject string
|
||||
err error
|
||||
extras []nestedError
|
||||
severity Severity
|
||||
subject string
|
||||
err error
|
||||
extras []nestedError
|
||||
}
|
||||
jsonNestedError struct {
|
||||
Subject string
|
||||
Err string
|
||||
Extras []jsonNestedError
|
||||
}
|
||||
Severity uint8
|
||||
)
|
||||
|
||||
const (
|
||||
SeverityWarning Severity = iota
|
||||
SeverityFatal
|
||||
)
|
||||
|
||||
func From(err error) NestedError {
|
||||
@@ -29,6 +28,29 @@ func From(err error) NestedError {
|
||||
return &nestedError{err: err}
|
||||
}
|
||||
|
||||
func FromJSON(data []byte) (NestedError, bool) {
|
||||
var j jsonNestedError
|
||||
if err := json.Unmarshal(data, &j); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
if j.Err == "" {
|
||||
return nil, false
|
||||
}
|
||||
extras := make([]nestedError, len(j.Extras))
|
||||
for i, e := range j.Extras {
|
||||
extra, ok := fromJSONObject(e)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
extras[i] = *extra
|
||||
}
|
||||
return &nestedError{
|
||||
subject: j.Subject,
|
||||
err: errors.New(j.Err),
|
||||
extras: extras,
|
||||
}, true
|
||||
}
|
||||
|
||||
// Check is a helper function that
|
||||
// convert (T, error) to (T, NestedError).
|
||||
func Check[T any](obj T, err error) (T, NestedError) {
|
||||
@@ -118,9 +140,9 @@ func (ne NestedError) With(s any) NestedError {
|
||||
case string:
|
||||
msg = ss
|
||||
case fmt.Stringer:
|
||||
return ne.append(ss.String())
|
||||
return ne.appendMsg(ss.String())
|
||||
default:
|
||||
return ne.append(fmt.Sprint(s))
|
||||
return ne.appendMsg(fmt.Sprint(s))
|
||||
}
|
||||
return ne.withError(From(errors.New(msg)))
|
||||
}
|
||||
@@ -133,13 +155,19 @@ func (ne NestedError) Subject(s any) NestedError {
|
||||
if ne == nil {
|
||||
return ne
|
||||
}
|
||||
var subject string
|
||||
switch ss := s.(type) {
|
||||
case string:
|
||||
ne.subject = ss
|
||||
subject = ss
|
||||
case fmt.Stringer:
|
||||
ne.subject = ss.String()
|
||||
subject = ss.String()
|
||||
default:
|
||||
ne.subject = fmt.Sprint(s)
|
||||
subject = fmt.Sprint(s)
|
||||
}
|
||||
if ne.subject == "" {
|
||||
ne.subject = subject
|
||||
} else {
|
||||
ne.subject = fmt.Sprintf("%s > %s", subject, ne.subject)
|
||||
}
|
||||
return ne
|
||||
}
|
||||
@@ -158,20 +186,21 @@ func (ne NestedError) Subjectf(format string, args ...any) NestedError {
|
||||
return ne
|
||||
}
|
||||
|
||||
func (ne NestedError) Severity(s Severity) NestedError {
|
||||
if ne == nil {
|
||||
return ne
|
||||
func (ne NestedError) JSONObject() jsonNestedError {
|
||||
extras := make([]jsonNestedError, len(ne.extras))
|
||||
for i, e := range ne.extras {
|
||||
extras[i] = e.JSONObject()
|
||||
}
|
||||
return jsonNestedError{
|
||||
Subject: ne.subject,
|
||||
Err: ne.err.Error(),
|
||||
Extras: extras,
|
||||
}
|
||||
ne.severity = s
|
||||
return ne
|
||||
}
|
||||
|
||||
func (ne NestedError) Warn() NestedError {
|
||||
if ne == nil {
|
||||
return ne
|
||||
}
|
||||
ne.severity = SeverityWarning
|
||||
return ne
|
||||
func (ne NestedError) JSON() []byte {
|
||||
b, _ := json.MarshalIndent(ne.JSONObject(), "", " ")
|
||||
return b
|
||||
}
|
||||
|
||||
func (ne NestedError) NoError() bool {
|
||||
@@ -182,18 +211,18 @@ func (ne NestedError) HasError() bool {
|
||||
return ne != nil
|
||||
}
|
||||
|
||||
func (ne NestedError) IsFatal() bool {
|
||||
return ne != nil && ne.severity == SeverityFatal
|
||||
}
|
||||
|
||||
func (ne NestedError) IsWarning() bool {
|
||||
return ne != nil && ne.severity == SeverityWarning
|
||||
}
|
||||
|
||||
func errorf(format string, args ...any) NestedError {
|
||||
return From(fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
func fromJSONObject(obj jsonNestedError) (NestedError, bool) {
|
||||
data, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return FromJSON(data)
|
||||
}
|
||||
|
||||
func (ne NestedError) withError(err NestedError) NestedError {
|
||||
if ne != nil && err != nil {
|
||||
ne.extras = append(ne.extras, *err)
|
||||
@@ -201,7 +230,7 @@ func (ne NestedError) withError(err NestedError) NestedError {
|
||||
return ne
|
||||
}
|
||||
|
||||
func (ne NestedError) append(msg string) NestedError {
|
||||
func (ne NestedError) appendMsg(msg string) NestedError {
|
||||
if ne == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -259,6 +288,8 @@ func (ne NestedError) buildError(level int, prefix string) error {
|
||||
for _, extra := range ne.extras {
|
||||
res = errors.Join(res, extra.buildError(level+1, "- "))
|
||||
}
|
||||
} else {
|
||||
res = fmt.Errorf("%w%s", res, sb.String())
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/error"
|
||||
. "github.com/yusing/go-proxy/utils/testing"
|
||||
. "github.com/yusing/go-proxy/internal/error"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestErrorIs(t *testing.T) {
|
||||
@@ -31,11 +31,11 @@ func TestErrorNestedIs(t *testing.T) {
|
||||
|
||||
err = Failure("some reason")
|
||||
ExpectTrue(t, err.Is(ErrFailure))
|
||||
ExpectFalse(t, err.Is(ErrAlreadyExist))
|
||||
ExpectFalse(t, err.Is(ErrDuplicated))
|
||||
|
||||
err.With(AlreadyExist("something", ""))
|
||||
err.With(Duplicated("something", ""))
|
||||
ExpectTrue(t, err.Is(ErrFailure))
|
||||
ExpectTrue(t, err.Is(ErrAlreadyExist))
|
||||
ExpectTrue(t, err.Is(ErrDuplicated))
|
||||
ExpectFalse(t, err.Is(ErrInvalid))
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailure = stderrors.New("failed")
|
||||
ErrInvalid = stderrors.New("invalid")
|
||||
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")
|
||||
ErrFailure = stderrors.New("failed")
|
||||
ErrInvalid = stderrors.New("invalid")
|
||||
ErrUnsupported = stderrors.New("unsupported")
|
||||
ErrUnexpected = stderrors.New("unexpected")
|
||||
ErrNotExists = stderrors.New("does not exist")
|
||||
ErrMissing = stderrors.New("missing")
|
||||
ErrDuplicated = stderrors.New("duplicated")
|
||||
ErrOutOfRange = stderrors.New("out of range")
|
||||
)
|
||||
|
||||
const fmtSubjectWhat = "%w %v: %q"
|
||||
@@ -53,8 +53,8 @@ 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 Duplicated(subject, what any) NestedError {
|
||||
return errorf("%w %v: %v", ErrDuplicated, subject, what)
|
||||
}
|
||||
|
||||
func OutOfRange(subject string, value any) NestedError {
|
||||
32
internal/http/content_type.go
Normal file
32
internal/http/content_type.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ContentType string
|
||||
|
||||
func GetContentType(h http.Header) ContentType {
|
||||
ct := h.Get("Content-Type")
|
||||
if ct == "" {
|
||||
return ""
|
||||
}
|
||||
ct, _, err := mime.ParseMediaType(ct)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return ContentType(ct)
|
||||
}
|
||||
|
||||
func (ct ContentType) IsHTML() bool {
|
||||
return ct == "text/html" || ct == "application/xhtml+xml"
|
||||
}
|
||||
|
||||
func (ct ContentType) IsJSON() bool {
|
||||
return ct == "application/json"
|
||||
}
|
||||
|
||||
func (ct ContentType) IsPlainText() bool {
|
||||
return ct == "text/plain"
|
||||
}
|
||||
42
internal/http/header_utils.go
Normal file
42
internal/http/header_utils.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func RemoveHop(h http.Header) {
|
||||
reqUpType := UpgradeType(h)
|
||||
RemoveHopByHopHeaders(h)
|
||||
|
||||
if reqUpType != "" {
|
||||
h.Set("Connection", "Upgrade")
|
||||
h.Set("Upgrade", reqUpType)
|
||||
} else {
|
||||
h.Del("Connection")
|
||||
}
|
||||
}
|
||||
|
||||
func CopyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FilterHeaders(h http.Header, allowed []string) {
|
||||
if allowed == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range allowed {
|
||||
allowed[i] = http.CanonicalHeaderKey(allowed[i])
|
||||
}
|
||||
|
||||
for key := range h {
|
||||
if !slices.Contains(allowed, key) {
|
||||
h.Del(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
96
internal/http/modify_response_writer.go
Normal file
96
internal/http/modify_response_writer.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Modified from Traefik Labs's MIT-licensed code (https://github.com/traefik/traefik/blob/master/pkg/middlewares/response_modifier.go)
|
||||
// Copyright (c) 2020-2024 Traefik Labs
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ModifyResponseFunc func(*http.Response) error
|
||||
type ModifyResponseWriter struct {
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
|
||||
headerSent bool
|
||||
code int
|
||||
|
||||
modifier ModifyResponseFunc
|
||||
modified bool
|
||||
modifierErr error
|
||||
}
|
||||
|
||||
func NewModifyResponseWriter(w http.ResponseWriter, r *http.Request, f ModifyResponseFunc) *ModifyResponseWriter {
|
||||
return &ModifyResponseWriter{
|
||||
w: w,
|
||||
r: r,
|
||||
modifier: f,
|
||||
code: http.StatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ModifyResponseWriter) WriteHeader(code int) {
|
||||
if w.headerSent {
|
||||
return
|
||||
}
|
||||
|
||||
if code >= http.StatusContinue && code < http.StatusOK {
|
||||
w.w.WriteHeader(code)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
w.headerSent = true
|
||||
w.code = code
|
||||
}()
|
||||
|
||||
if w.modifier == nil || w.modified {
|
||||
w.w.WriteHeader(code)
|
||||
return
|
||||
}
|
||||
|
||||
resp := http.Response{
|
||||
Header: w.w.Header(),
|
||||
Request: w.r,
|
||||
}
|
||||
|
||||
if err := w.modifier(&resp); err != nil {
|
||||
w.modifierErr = err
|
||||
logger.Errorf("error modifying response: %s", err)
|
||||
w.w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.modified = true
|
||||
w.w.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (w *ModifyResponseWriter) Header() http.Header {
|
||||
return w.w.Header()
|
||||
}
|
||||
|
||||
func (w *ModifyResponseWriter) Write(b []byte) (int, error) {
|
||||
w.WriteHeader(w.code)
|
||||
if w.modifierErr != nil {
|
||||
return 0, w.modifierErr
|
||||
}
|
||||
return w.w.Write(b)
|
||||
}
|
||||
|
||||
// Hijack hijacks the connection.
|
||||
func (w *ModifyResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if h, ok := w.w.(http.Hijacker); ok {
|
||||
return h.Hijack()
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("not a hijacker: %T", w.w)
|
||||
}
|
||||
|
||||
// Flush sends any buffered data to the client.
|
||||
func (w *ModifyResponseWriter) Flush() {
|
||||
if flusher, ok := w.w.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
package proxy
|
||||
// Copyright 2011 The Go Authors.
|
||||
// Modified from the Go project under the a BSD-style License (https://cs.opensource.google/go/go/+/refs/tags/go1.23.1:src/net/http/httputil/reverseproxy.go)
|
||||
// https://cs.opensource.google/go/go/+/master:LICENSE
|
||||
|
||||
// A small mod on net/http/httputil/reverseproxy.go
|
||||
// that doubled the performance
|
||||
package http
|
||||
|
||||
// This is a small mod on net/http/httputil/reverseproxy.go
|
||||
// that boosts performance in some cases
|
||||
// and compatible to other modules of this project
|
||||
// Copyright (c) 2024 yusing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -52,6 +59,21 @@ type ProxyRequest struct {
|
||||
// r.SetXForwarded()
|
||||
// }
|
||||
func (r *ProxyRequest) SetXForwarded() {
|
||||
clientIP, _, err := net.SplitHostPort(r.In.RemoteAddr)
|
||||
if err == nil {
|
||||
r.Out.Header.Set("X-Forwarded-For", clientIP)
|
||||
} else {
|
||||
r.Out.Header.Del("X-Forwarded-For")
|
||||
}
|
||||
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
|
||||
if r.In.TLS == nil {
|
||||
r.Out.Header.Set("X-Forwarded-Proto", "http")
|
||||
} else {
|
||||
r.Out.Header.Set("X-Forwarded-Proto", "https")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProxyRequest) AddXForwarded() {
|
||||
clientIP, _, err := net.SplitHostPort(r.In.RemoteAddr)
|
||||
if err == nil {
|
||||
prior := r.Out.Header["X-Forwarded-For"]
|
||||
@@ -104,45 +126,18 @@ type ReverseProxy struct {
|
||||
// If nil, http.DefaultTransport is used.
|
||||
Transport http.RoundTripper
|
||||
|
||||
// FlushInterval specifies the flush interval
|
||||
// to flush to the client while copying the
|
||||
// response body.
|
||||
// If zero, no periodic flushing is done.
|
||||
// A negative value means to flush immediately
|
||||
// after each write to the client.
|
||||
// The FlushInterval is ignored when ReverseProxy
|
||||
// recognizes a response as a streaming response, or
|
||||
// if its ContentLength is -1; for such responses, writes
|
||||
// are flushed to the client immediately.
|
||||
// FlushInterval time.Duration
|
||||
|
||||
// ErrorLog specifies an optional logger for errors
|
||||
// that occur when attempting to proxy the request.
|
||||
// If nil, logging is done via the log package's standard logger.
|
||||
// ErrorLog *log.Logger
|
||||
|
||||
// BufferPool optionally specifies a buffer pool to
|
||||
// get byte slices for use by io.CopyBuffer when
|
||||
// copying HTTP response bodies.
|
||||
// BufferPool BufferPool
|
||||
|
||||
// ModifyResponse is an optional function that modifies the
|
||||
// Response from the backend. It is called if the backend
|
||||
// returns a response at all, with any HTTP status code.
|
||||
// If the backend is unreachable, the optional ErrorHandler is
|
||||
// called without any call to ModifyResponse.
|
||||
// called before ModifyResponse.
|
||||
//
|
||||
// If ModifyResponse returns an error, ErrorHandler is called
|
||||
// with its error value. If ErrorHandler is nil, its default
|
||||
// implementation is used.
|
||||
ModifyResponse func(*http.Response) error
|
||||
|
||||
// ErrorHandler is an optional function that handles errors
|
||||
// reaching the backend or errors from ModifyResponse.
|
||||
//
|
||||
// If nil, the default is to log the provided error and return
|
||||
// a 502 Status Bad Gateway response.
|
||||
ErrorHandler func(http.ResponseWriter, *http.Request, error)
|
||||
ServeHTTP http.HandlerFunc
|
||||
}
|
||||
|
||||
// A BufferPool is an interface for getting and returning temporary
|
||||
@@ -206,36 +201,15 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// TODO: headers in ModifyResponse
|
||||
func NewReverseProxy(target *url.URL, transport http.RoundTripper, entry *ReverseProxyEntry) *ReverseProxy {
|
||||
// check on init rather than on request
|
||||
var setHeaders = func(r *http.Request) {}
|
||||
var hideHeaders = func(r *http.Request) {}
|
||||
if len(entry.SetHeaders) > 0 {
|
||||
setHeaders = func(r *http.Request) {
|
||||
h := entry.SetHeaders.Clone()
|
||||
for k, vv := range h {
|
||||
if k == "Host" {
|
||||
r.Host = vv[0]
|
||||
} else {
|
||||
r.Header[k] = vv
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewReverseProxy(target *url.URL, transport http.RoundTripper) *ReverseProxy {
|
||||
rp := &ReverseProxy{
|
||||
Rewrite: func(pr *ProxyRequest) {
|
||||
rewriteRequestURL(pr.Out, target)
|
||||
}, Transport: transport,
|
||||
}
|
||||
if len(entry.HideHeaders) > 0 {
|
||||
hideHeaders = func(r *http.Request) {
|
||||
for _, k := range entry.HideHeaders {
|
||||
r.Header.Del(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &ReverseProxy{Rewrite: func(pr *ProxyRequest) {
|
||||
rewriteRequestURL(pr.Out, target)
|
||||
// pr.SetXForwarded()
|
||||
setHeaders(pr.Out)
|
||||
hideHeaders(pr.Out)
|
||||
}, Transport: transport}
|
||||
rp.ServeHTTP = rp.serveHTTP
|
||||
return rp
|
||||
}
|
||||
|
||||
func rewriteRequestURL(req *http.Request, target *url.URL) {
|
||||
@@ -250,6 +224,23 @@ func rewriteRequestURL(req *http.Request, target *url.URL) {
|
||||
}
|
||||
}
|
||||
|
||||
// Hop-by-hop headers. These are removed when sent to the backend.
|
||||
// As of RFC 7230, hop-by-hop headers are required to appear in the
|
||||
// Connection header field. These are the headers defined by the
|
||||
// obsoleted RFC 2616 (section 13.5.1) and are used for backward
|
||||
// compatibility.
|
||||
var hopHeaders = []string{
|
||||
"Connection",
|
||||
"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
|
||||
"Keep-Alive",
|
||||
"Proxy-Authenticate",
|
||||
"Proxy-Authorization",
|
||||
"Te", // canonicalized version of "TE"
|
||||
"Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522
|
||||
"Transfer-Encoding",
|
||||
"Upgrade",
|
||||
}
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
@@ -258,9 +249,11 @@ func copyHeader(dst, src http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) errorHandler(rw http.ResponseWriter, _ *http.Request, err error) {
|
||||
logger.Errorf("http: proxy error: %s", err)
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
func (p *ReverseProxy) errorHandler(rw http.ResponseWriter, r *http.Request, err error, writeHeader bool) {
|
||||
logger.Errorf("http proxy to %s failed: %s", r.URL.String(), err)
|
||||
if writeHeader {
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
}
|
||||
}
|
||||
|
||||
// modifyResponse conditionally runs the optional ModifyResponse hook
|
||||
@@ -271,13 +264,13 @@ func (p *ReverseProxy) modifyResponse(rw http.ResponseWriter, res *http.Response
|
||||
}
|
||||
if err := p.ModifyResponse(res); err != nil {
|
||||
res.Body.Close()
|
||||
p.errorHandler(rw, req, err)
|
||||
p.errorHandler(rw, req, err, true)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
transport := p.Transport
|
||||
|
||||
ctx := req.Context()
|
||||
@@ -325,12 +318,14 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
outreq.Close = false
|
||||
|
||||
reqUpType := upgradeType(outreq.Header)
|
||||
reqUpType := UpgradeType(outreq.Header)
|
||||
if !IsPrint(reqUpType) {
|
||||
p.errorHandler(rw, req, fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType))
|
||||
p.errorHandler(rw, req, fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType), true)
|
||||
return
|
||||
}
|
||||
|
||||
RemoveHopByHopHeaders(outreq.Header)
|
||||
|
||||
// Issue 21096: tell backend applications that care about trailer support
|
||||
// that we support trailers. (We do, but we don't go out of our way to
|
||||
// advertise that unless the incoming client request thought it was worth
|
||||
@@ -348,9 +343,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,
|
||||
@@ -385,8 +380,20 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
res, err := transport.RoundTrip(outreq)
|
||||
if err != nil {
|
||||
p.errorHandler(rw, outreq, err)
|
||||
return
|
||||
p.errorHandler(rw, outreq, err, false)
|
||||
errMsg := err.Error()
|
||||
res = &http.Response{
|
||||
Status: http.StatusText(http.StatusBadGateway),
|
||||
StatusCode: http.StatusBadGateway,
|
||||
Proto: outreq.Proto,
|
||||
ProtoMajor: outreq.ProtoMajor,
|
||||
ProtoMinor: outreq.ProtoMinor,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(errMsg))),
|
||||
Request: outreq,
|
||||
ContentLength: int64(len(errMsg)),
|
||||
TLS: outreq.TLS,
|
||||
}
|
||||
}
|
||||
|
||||
// Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
|
||||
@@ -419,16 +426,9 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
_, err = io.Copy(rw, res.Body)
|
||||
if err != nil {
|
||||
defer res.Body.Close()
|
||||
// note: removed
|
||||
// Since we're streaming the response, if we run into an error all we can do
|
||||
// is abort the request. Issue 23643: ReverseProxy should use ErrAbortHandler
|
||||
// on read error while copying body.
|
||||
// if !shouldPanicOnCopyError(req) {
|
||||
// p.logf("suppressing panic for copyResponse error in test; copy error: %s", err)
|
||||
// return
|
||||
// }
|
||||
panic(http.ErrAbortHandler)
|
||||
p.errorHandler(rw, req, err, true)
|
||||
res.Body.Close()
|
||||
return
|
||||
}
|
||||
res.Body.Close() // close now, instead of defer, to populate res.Trailer
|
||||
|
||||
@@ -452,34 +452,52 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func upgradeType(h http.Header) string {
|
||||
func UpgradeType(h http.Header) string {
|
||||
if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") {
|
||||
return ""
|
||||
}
|
||||
return h.Get("Upgrade")
|
||||
}
|
||||
|
||||
// RemoveHopByHopHeaders removes hop-by-hop headers.
|
||||
func RemoveHopByHopHeaders(h http.Header) {
|
||||
// RFC 7230, section 6.1: Remove headers listed in the "Connection" header.
|
||||
for _, f := range h["Connection"] {
|
||||
for _, sf := range strings.Split(f, ",") {
|
||||
if sf = textproto.TrimString(sf); sf != "" {
|
||||
h.Del(sf)
|
||||
}
|
||||
}
|
||||
}
|
||||
// RFC 2616, section 13.5.1: Remove a set of known hop-by-hop headers.
|
||||
// This behavior is superseded by the RFC 7230 Connection header, but
|
||||
// preserve it for backwards compatibility.
|
||||
for _, f := range hopHeaders {
|
||||
h.Del(f)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.Request, res *http.Response) {
|
||||
reqUpType := upgradeType(req.Header)
|
||||
resUpType := upgradeType(res.Header)
|
||||
reqUpType := UpgradeType(req.Header)
|
||||
resUpType := UpgradeType(res.Header)
|
||||
if !IsPrint(resUpType) { // We know reqUpType is ASCII, it's checked by the caller.
|
||||
p.errorHandler(rw, req, fmt.Errorf("backend tried to switch to invalid protocol %q", resUpType))
|
||||
p.errorHandler(rw, req, fmt.Errorf("backend tried to switch to invalid protocol %q", resUpType), true)
|
||||
}
|
||||
if !strings.EqualFold(reqUpType, resUpType) {
|
||||
p.errorHandler(rw, req, fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType))
|
||||
p.errorHandler(rw, req, fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType), true)
|
||||
return
|
||||
}
|
||||
|
||||
backConn, ok := res.Body.(io.ReadWriteCloser)
|
||||
if !ok {
|
||||
p.errorHandler(rw, req, fmt.Errorf("internal error: 101 switching protocols response with non-writable body"))
|
||||
p.errorHandler(rw, req, fmt.Errorf("internal error: 101 switching protocols response with non-writable body"), true)
|
||||
return
|
||||
}
|
||||
|
||||
rc := http.NewResponseController(rw)
|
||||
conn, brw, hijackErr := rc.Hijack()
|
||||
if errors.Is(hijackErr, http.ErrNotSupported) {
|
||||
p.errorHandler(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw))
|
||||
p.errorHandler(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw), true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -496,7 +514,7 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R
|
||||
defer close(backConnCloseCh)
|
||||
|
||||
if hijackErr != nil {
|
||||
p.errorHandler(rw, req, fmt.Errorf("hijack failed on protocol switch: %w", hijackErr))
|
||||
p.errorHandler(rw, req, fmt.Errorf("hijack failed on protocol switch: %w", hijackErr), true)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
@@ -506,11 +524,11 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R
|
||||
res.Header = rw.Header()
|
||||
res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above
|
||||
if err := res.Write(brw); err != nil {
|
||||
p.errorHandler(rw, req, fmt.Errorf("response write: %s", err))
|
||||
p.errorHandler(rw, req, fmt.Errorf("response write: %s", err), true)
|
||||
return
|
||||
}
|
||||
if err := brw.Flush(); err != nil {
|
||||
p.errorHandler(rw, req, fmt.Errorf("response flush: %s", err))
|
||||
p.errorHandler(rw, req, fmt.Errorf("response flush: %s", err), true)
|
||||
return
|
||||
}
|
||||
errc := make(chan error, 1)
|
||||
7
internal/http/status_code.go
Normal file
7
internal/http/status_code.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
func IsSuccess(status int) bool {
|
||||
return status >= http.StatusOK && status < http.StatusMultipleChoices
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package model
|
||||
type Config struct {
|
||||
Providers ProxyProviders `yaml:",flow" json:"providers"`
|
||||
AutoCert AutoCertConfig `yaml:",flow" json:"autocert"`
|
||||
ExplicitOnly bool `yaml:"explicit_only" json:"explicit_only"`
|
||||
MatchDomains []string `yaml:"match_domains" json:"match_domains"`
|
||||
TimeoutShutdown int `yaml:"timeout_shutdown" json:"timeout_shutdown"`
|
||||
RedirectToHTTPS bool `yaml:"redirect_to_https" json:"redirect_to_https"`
|
||||
}
|
||||
@@ -11,6 +13,6 @@ func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Providers: ProxyProviders{},
|
||||
TimeoutShutdown: 3,
|
||||
RedirectToHTTPS: true,
|
||||
RedirectToHTTPS: false,
|
||||
}
|
||||
}
|
||||
151
internal/models/raw_entry.go
Normal file
151
internal/models/raw_entry.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/common"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
)
|
||||
|
||||
type (
|
||||
RawEntry struct {
|
||||
// raw entry object before validation
|
||||
// loaded from docker labels or yaml file
|
||||
Alias string `yaml:"-" json:"-"`
|
||||
Scheme string `yaml:"scheme" json:"scheme"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
Port string `yaml:"port" json:"port"`
|
||||
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // https proxy only
|
||||
PathPatterns []string `yaml:"path_patterns" json:"path_patterns"` // http(s) proxy only
|
||||
Middlewares D.NestedLabelMap `yaml:"middlewares" json:"middlewares"`
|
||||
|
||||
/* Docker only */
|
||||
*D.ProxyProperties `yaml:"-" json:"proxy_properties"`
|
||||
}
|
||||
|
||||
RawEntries = F.Map[string, *RawEntry]
|
||||
)
|
||||
|
||||
var NewProxyEntries = F.NewMapOf[string, *RawEntry]
|
||||
|
||||
func (e *RawEntry) FillMissingFields() bool {
|
||||
isDocker := e.ProxyProperties != nil
|
||||
if !isDocker {
|
||||
e.ProxyProperties = &D.ProxyProperties{}
|
||||
}
|
||||
|
||||
lp, pp, extra := e.splitPorts()
|
||||
|
||||
if port, ok := ServiceNamePortMapTCP[e.ImageName]; ok {
|
||||
if pp == "" {
|
||||
pp = strconv.Itoa(port)
|
||||
}
|
||||
if e.Scheme == "" {
|
||||
e.Scheme = "tcp"
|
||||
}
|
||||
} else if port, ok := ImageNamePortMap[e.ImageName]; ok {
|
||||
if pp == "" {
|
||||
pp = strconv.Itoa(port)
|
||||
}
|
||||
if e.Scheme == "" {
|
||||
e.Scheme = "http"
|
||||
}
|
||||
} else if pp == "" && e.Scheme == "https" {
|
||||
pp = "443"
|
||||
} else if pp == "" {
|
||||
if p, ok := F.FirstValueOf(e.PrivatePortMapping); ok {
|
||||
pp = fmt.Sprint(p.PrivatePort)
|
||||
} else {
|
||||
pp = "80"
|
||||
}
|
||||
}
|
||||
|
||||
// replace private port with public port (if any)
|
||||
if isDocker && e.NetworkMode != "host" {
|
||||
if p, ok := e.PrivatePortMapping[pp]; ok {
|
||||
pp = fmt.Sprint(p.PublicPort)
|
||||
}
|
||||
if _, ok := e.PublicPortMapping[pp]; !ok { // port is not exposed, but specified
|
||||
// try to fallback to first public port
|
||||
if p, ok := F.FirstValueOf(e.PublicPortMapping); ok {
|
||||
pp = fmt.Sprint(p.PublicPort)
|
||||
}
|
||||
// ignore only if it is NOT RUNNING
|
||||
// because stopped containers
|
||||
// will have empty port mapping got from docker
|
||||
if e.Running {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if e.Scheme == "" && isDocker {
|
||||
if p, ok := e.PublicPortMapping[pp]; ok && p.Type == "udp" {
|
||||
e.Scheme = "udp"
|
||||
}
|
||||
}
|
||||
|
||||
if e.Scheme == "" {
|
||||
if lp != "" {
|
||||
e.Scheme = "tcp"
|
||||
} else if strings.HasSuffix(pp, "443") {
|
||||
e.Scheme = "https"
|
||||
} else if _, ok := WellKnownHTTPPorts[pp]; ok {
|
||||
e.Scheme = "http"
|
||||
} else {
|
||||
// assume its http
|
||||
e.Scheme = "http"
|
||||
}
|
||||
}
|
||||
|
||||
if e.Host == "" {
|
||||
e.Host = "localhost"
|
||||
}
|
||||
if e.IdleTimeout == "" {
|
||||
e.IdleTimeout = IdleTimeoutDefault
|
||||
}
|
||||
if e.WakeTimeout == "" {
|
||||
e.WakeTimeout = WakeTimeoutDefault
|
||||
}
|
||||
if e.StopTimeout == "" {
|
||||
e.StopTimeout = StopTimeoutDefault
|
||||
}
|
||||
if e.StopMethod == "" {
|
||||
e.StopMethod = StopMethodDefault
|
||||
}
|
||||
|
||||
e.Port = joinPorts(lp, pp, extra)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *RawEntry) splitPorts() (lp string, pp string, extra string) {
|
||||
portSplit := strings.Split(e.Port, ":")
|
||||
if len(portSplit) == 1 {
|
||||
pp = portSplit[0]
|
||||
} else {
|
||||
lp = portSplit[0]
|
||||
pp = portSplit[1]
|
||||
}
|
||||
if len(portSplit) > 2 {
|
||||
extra = strings.Join(portSplit[2:], ":")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func joinPorts(lp string, pp string, extra string) string {
|
||||
s := make([]string, 0, 3)
|
||||
if lp != "" {
|
||||
s = append(s, lp)
|
||||
}
|
||||
if pp != "" {
|
||||
s = append(s, pp)
|
||||
}
|
||||
if extra != "" {
|
||||
s = append(s, extra)
|
||||
}
|
||||
return strings.Join(s, ":")
|
||||
}
|
||||
@@ -2,13 +2,13 @@ package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
T "github.com/yusing/go-proxy/proxy/fields"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
M "github.com/yusing/go-proxy/internal/models"
|
||||
T "github.com/yusing/go-proxy/internal/proxy/fields"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -18,8 +18,7 @@ type (
|
||||
URL *url.URL
|
||||
NoTLSVerify bool
|
||||
PathPatterns T.PathPatterns
|
||||
SetHeaders http.Header
|
||||
HideHeaders []string
|
||||
Middlewares D.NestedLabelMap
|
||||
|
||||
/* Docker only */
|
||||
IdleTimeout time.Duration
|
||||
@@ -54,7 +53,7 @@ func ValidateEntry(m *M.RawEntry) (any, E.NestedError) {
|
||||
}
|
||||
|
||||
var entry any
|
||||
e := E.NewBuilder("error validating proxy entry")
|
||||
e := E.NewBuilder("error validating entry")
|
||||
if scheme.IsStream() {
|
||||
entry = validateStreamEntry(m, e)
|
||||
} else {
|
||||
@@ -78,9 +77,6 @@ func validateRPEntry(m *M.RawEntry, s T.Scheme, b E.Builder) *ReverseProxyEntry
|
||||
pathPatterns, err := T.ValidatePathPatterns(m.PathPatterns)
|
||||
b.Add(err)
|
||||
|
||||
setHeaders, err := T.ValidateHTTPHeaders(m.SetHeaders)
|
||||
b.Add(err)
|
||||
|
||||
url, err := E.Check(url.Parse(fmt.Sprintf("%s://%s:%d", s, host, port)))
|
||||
b.Add(err)
|
||||
|
||||
@@ -111,8 +107,7 @@ func validateRPEntry(m *M.RawEntry, s T.Scheme, b E.Builder) *ReverseProxyEntry
|
||||
URL: url,
|
||||
NoTLSVerify: m.NoTLSVerify,
|
||||
PathPatterns: pathPatterns,
|
||||
SetHeaders: setHeaders,
|
||||
HideHeaders: m.HideHeaders,
|
||||
Middlewares: m.Middlewares,
|
||||
IdleTimeout: idleTimeout,
|
||||
WakeTimeout: wakeTimeout,
|
||||
StopMethod: stopMethod,
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
func ValidateHTTPHeaders(headers map[string]string) (http.Header, E.NestedError) {
|
||||
12
internal/proxy/fields/host.go
Normal file
12
internal/proxy/fields/host.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type Host string
|
||||
type Subdomain = Alias
|
||||
|
||||
func ValidateHost[String ~string](s String) (Host, E.NestedError) {
|
||||
return Host(s), nil
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type PathMode string
|
||||
@@ -3,7 +3,7 @@ package fields
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type PathPattern string
|
||||
@@ -3,8 +3,8 @@ package fields
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
U "github.com/yusing/go-proxy/utils/testing"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
U "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
var validPatterns = []string{
|
||||
@@ -3,13 +3,13 @@ package fields
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type Port int
|
||||
|
||||
func ValidatePort(v string) (Port, E.NestedError) {
|
||||
p, err := strconv.Atoi(v)
|
||||
func ValidatePort[String ~string](v String) (Port, E.NestedError) {
|
||||
p, err := strconv.Atoi(string(v))
|
||||
if err != nil {
|
||||
return ErrPort, E.Invalid("port number", v).With(err)
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type Scheme string
|
||||
|
||||
func NewScheme(s string) (Scheme, E.NestedError) {
|
||||
func NewScheme[String ~string](s String) (Scheme, E.NestedError) {
|
||||
switch s {
|
||||
case "http", "https", "tcp", "udp":
|
||||
return Scheme(s), nil
|
||||
@@ -1,7 +1,7 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type Signal string
|
||||
@@ -1,7 +1,7 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type StopMethod string
|
||||
@@ -3,8 +3,8 @@ package fields
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/common"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type StreamPort struct {
|
||||
@@ -26,18 +26,19 @@ func ValidateStreamPort(p string) (StreamPort, E.NestedError) {
|
||||
|
||||
listeningPort, err := ValidatePort(split[0])
|
||||
if err != nil {
|
||||
return ErrStreamPort, err
|
||||
return ErrStreamPort, err.Subject("listening port")
|
||||
}
|
||||
|
||||
proxyPort, err := ValidatePort(split[1])
|
||||
|
||||
if err.Is(E.ErrOutOfRange) {
|
||||
return ErrStreamPort, err
|
||||
return ErrStreamPort, err.Subject("proxy port")
|
||||
} else if proxyPort == 0 {
|
||||
return ErrStreamPort, E.Invalid("stream port", p).With("proxy port cannot be 0")
|
||||
return ErrStreamPort, E.Invalid("proxy port", p)
|
||||
} else if err != nil {
|
||||
proxyPort, err = parseNameToPort(split[1])
|
||||
if err != nil {
|
||||
return ErrStreamPort, E.Invalid("stream port", p).With(proxyPort)
|
||||
return ErrStreamPort, E.Invalid("proxy port", proxyPort)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package fields
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
. "github.com/yusing/go-proxy/utils/testing"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
var validPorts = []string{
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type StreamScheme struct {
|
||||
@@ -3,7 +3,7 @@ package fields
|
||||
import (
|
||||
"time"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
func ValidateDurationPostitive(value string) (time.Duration, E.NestedError) {
|
||||
@@ -6,25 +6,28 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
D "github.com/yusing/go-proxy/docker"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
"github.com/sirupsen/logrus"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
M "github.com/yusing/go-proxy/internal/models"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
)
|
||||
|
||||
type DockerProvider struct {
|
||||
dockerHost, hostname string
|
||||
ExplicitOnly bool
|
||||
}
|
||||
|
||||
var AliasRefRegex = regexp.MustCompile(`\$\d+`)
|
||||
var AliasRefRegex = regexp.MustCompile(`#\d+`)
|
||||
var AliasRefRegexOld = regexp.MustCompile(`\$\d+`)
|
||||
|
||||
func DockerProviderImpl(dockerHost string) (ProviderImpl, E.NestedError) {
|
||||
func DockerProviderImpl(dockerHost string, explicitOnly bool) (ProviderImpl, E.NestedError) {
|
||||
hostname, err := D.ParseDockerHostname(dockerHost)
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
return &DockerProvider{dockerHost: dockerHost, hostname: hostname}, nil
|
||||
return &DockerProvider{dockerHost, hostname, explicitOnly}, nil
|
||||
}
|
||||
|
||||
func (p *DockerProvider) String() string {
|
||||
@@ -103,7 +106,7 @@ func (p *DockerProvider) OnEvent(event W.Event, routes R.Routes) (res EventResul
|
||||
|
||||
entries.RangeAll(func(alias string, entry *M.RawEntry) {
|
||||
if routes.Has(alias) {
|
||||
b.Add(E.AlreadyExist("alias", alias))
|
||||
b.Add(E.Duplicated("alias", alias))
|
||||
} else {
|
||||
if route, err := R.NewRoute(entry); err.HasError() {
|
||||
b.Add(err)
|
||||
@@ -120,8 +123,13 @@ func (p *DockerProvider) OnEvent(event W.Event, routes R.Routes) (res EventResul
|
||||
|
||||
// Returns a list of proxy entries for a container.
|
||||
// Always non-nil
|
||||
func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.RawEntries, E.NestedError) {
|
||||
entries := M.NewProxyEntries()
|
||||
func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (entries M.RawEntries, _ E.NestedError) {
|
||||
entries = M.NewProxyEntries()
|
||||
|
||||
if container.IsExcluded ||
|
||||
!container.IsExplicit && p.ExplicitOnly {
|
||||
return
|
||||
}
|
||||
|
||||
// init entries map for all aliases
|
||||
for _, a := range container.Aliases {
|
||||
@@ -137,30 +145,11 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.Ra
|
||||
errors.Add(p.applyLabel(container, entries, key, val))
|
||||
}
|
||||
|
||||
// selecting correct host port
|
||||
replacePrivPorts := func() {
|
||||
if container.HostConfig.NetworkMode != "host" {
|
||||
entries.RangeAll(func(_ string, entry *M.RawEntry) {
|
||||
entryPortSplit := strings.Split(entry.Port, ":")
|
||||
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, ":")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -168,6 +157,20 @@ func (p *DockerProvider) applyLabel(container D.Container, entries M.RawEntries,
|
||||
b := E.NewBuilder("errors in label %s", key)
|
||||
defer b.To(&res)
|
||||
|
||||
refErr := E.NewBuilder("errors parsing alias references")
|
||||
replaceIndexRef := func(ref string) string {
|
||||
index, err := strconv.Atoi(ref[1:])
|
||||
if err != nil {
|
||||
refErr.Add(E.Invalid("integer", ref))
|
||||
return ref
|
||||
}
|
||||
if index < 1 || index > len(container.Aliases) {
|
||||
refErr.Add(E.OutOfRange("index", ref))
|
||||
return ref
|
||||
}
|
||||
return container.Aliases[index-1]
|
||||
}
|
||||
|
||||
lbl, err := D.ParseLabel(key, val)
|
||||
if err.HasError() {
|
||||
b.Add(err.Subject(key))
|
||||
@@ -179,22 +182,14 @@ func (p *DockerProvider) applyLabel(container D.Container, entries M.RawEntries,
|
||||
// apply label for all aliases
|
||||
entries.RangeAll(func(a string, e *M.RawEntry) {
|
||||
if err = D.ApplyLabel(e, lbl); err.HasError() {
|
||||
b.Add(err.Subject(lbl.Target))
|
||||
b.Add(err.Subjectf("alias %s", lbl.Target))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
refErr := E.NewBuilder("errors parsing alias references")
|
||||
lbl.Target = AliasRefRegex.ReplaceAllStringFunc(lbl.Target, func(ref string) string {
|
||||
index, err := strconv.Atoi(ref[1:])
|
||||
if err != nil {
|
||||
refErr.Add(E.Invalid("integer", ref))
|
||||
return ref
|
||||
}
|
||||
if index < 1 || index > len(container.Aliases) {
|
||||
refErr.Add(E.OutOfRange("index", ref))
|
||||
return ref
|
||||
}
|
||||
return container.Aliases[index-1]
|
||||
lbl.Target = AliasRefRegex.ReplaceAllStringFunc(lbl.Target, replaceIndexRef)
|
||||
lbl.Target = AliasRefRegexOld.ReplaceAllStringFunc(lbl.Target, func(s string) string {
|
||||
logrus.Warnf("%q should now be %q, old syntax will be removed in a future version", lbl, strings.ReplaceAll(lbl.String(), "$", "#"))
|
||||
return replaceIndexRef(s)
|
||||
})
|
||||
if refErr.HasError() {
|
||||
b.Add(refErr.Build())
|
||||
@@ -206,7 +201,7 @@ func (p *DockerProvider) applyLabel(container D.Container, entries M.RawEntries,
|
||||
return
|
||||
}
|
||||
if err = D.ApplyLabel(config, lbl); err.HasError() {
|
||||
b.Add(err.Subject(lbl.Target))
|
||||
b.Add(err.Subjectf("alias %s", lbl.Target))
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/yusing/go-proxy/common"
|
||||
D "github.com/yusing/go-proxy/docker"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
P "github.com/yusing/go-proxy/proxy"
|
||||
T "github.com/yusing/go-proxy/proxy/fields"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
P "github.com/yusing/go-proxy/internal/proxy"
|
||||
T "github.com/yusing/go-proxy/internal/proxy/fields"
|
||||
|
||||
. "github.com/yusing/go-proxy/utils/testing"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
var dummyNames = []string{"/a"}
|
||||
@@ -27,41 +27,36 @@ func TestApplyLabelFieldValidity(t *testing.T) {
|
||||
"POST /upload/{$}",
|
||||
"GET /static",
|
||||
}
|
||||
setHeaders := `
|
||||
X_Custom_Header1: value1
|
||||
X_Custom_Header1: value2
|
||||
X_Custom_Header2: value3
|
||||
`[1:]
|
||||
setHeadersExpect := map[string]string{
|
||||
"X_Custom_Header1": "value1, value2",
|
||||
"X_Custom_Header2": "value3",
|
||||
}
|
||||
hideHeaders := `
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
`[1:]
|
||||
hideHeadersExpect := []string{
|
||||
"X-Custom-Header1",
|
||||
"X-Custom-Header2",
|
||||
middlewaresExpect := D.NestedLabelMap{
|
||||
"middleware1": {
|
||||
"prop1": "value1",
|
||||
"prop2": "value2",
|
||||
},
|
||||
"middleware2": {
|
||||
"prop3": "value3",
|
||||
"prop4": "value4",
|
||||
},
|
||||
}
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
D.LabelIdleTimeout: common.IdleTimeoutDefault,
|
||||
D.LabelStopMethod: common.StopMethodDefault,
|
||||
D.LabelStopSignal: "SIGTERM",
|
||||
D.LabelStopTimeout: common.StopTimeoutDefault,
|
||||
D.LabelWakeTimeout: common.WakeTimeoutDefault,
|
||||
"proxy.*.no_tls_verify": "true",
|
||||
"proxy.*.scheme": "https",
|
||||
"proxy.*.host": "app",
|
||||
"proxy.*.port": "4567",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
"proxy.a.path_patterns": pathPatterns,
|
||||
"proxy.a.set_headers": setHeaders,
|
||||
"proxy.a.hide_headers": hideHeaders,
|
||||
D.LabelAliases: "a,b",
|
||||
D.LabelIdleTimeout: common.IdleTimeoutDefault,
|
||||
D.LabelStopMethod: common.StopMethodDefault,
|
||||
D.LabelStopSignal: "SIGTERM",
|
||||
D.LabelStopTimeout: common.StopTimeoutDefault,
|
||||
D.LabelWakeTimeout: common.WakeTimeoutDefault,
|
||||
"proxy.*.no_tls_verify": "true",
|
||||
"proxy.*.scheme": "https",
|
||||
"proxy.*.host": "app",
|
||||
"proxy.*.port": "4567",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
"proxy.a.path_patterns": pathPatterns,
|
||||
"proxy.a.middlewares.middleware1.prop1": "value1",
|
||||
"proxy.a.middlewares.middleware1.prop2": "value2",
|
||||
"proxy.a.middlewares.middleware2.prop3": "value3",
|
||||
"proxy.a.middlewares.middleware2.prop4": "value4",
|
||||
},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 4567, PublicPort: 8888},
|
||||
@@ -88,11 +83,8 @@ X_Custom_Header2: value3
|
||||
ExpectDeepEqual(t, a.PathPatterns, pathPatternsExpect)
|
||||
ExpectEqual(t, len(b.PathPatterns), 0)
|
||||
|
||||
ExpectDeepEqual(t, a.SetHeaders, setHeadersExpect)
|
||||
ExpectEqual(t, len(b.SetHeaders), 0)
|
||||
|
||||
ExpectDeepEqual(t, a.HideHeaders, hideHeadersExpect)
|
||||
ExpectEqual(t, len(b.HideHeaders), 0)
|
||||
ExpectDeepEqual(t, a.Middlewares, middlewaresExpect)
|
||||
ExpectEqual(t, len(b.Middlewares), 0)
|
||||
|
||||
ExpectEqual(t, a.IdleTimeout, common.IdleTimeoutDefault)
|
||||
ExpectEqual(t, b.IdleTimeout, common.IdleTimeoutDefault)
|
||||
@@ -140,7 +132,8 @@ func TestApplyLabel(t *testing.T) {
|
||||
ExpectEqual(t, b.Scheme, "http")
|
||||
ExpectEqual(t, b.Port, "1234")
|
||||
ExpectEqual(t, c.Scheme, "https")
|
||||
ExpectEqual(t, c.Port, "1111")
|
||||
// map does not necessary follow the order above
|
||||
ExpectEqualAny(t, c.Port, []string{"1111", "1234"})
|
||||
}
|
||||
|
||||
func TestApplyLabelWithRef(t *testing.T) {
|
||||
@@ -149,11 +142,11 @@ func TestApplyLabelWithRef(t *testing.T) {
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b,c",
|
||||
"proxy.$1.host": "localhost",
|
||||
"proxy.*.port": "1111",
|
||||
"proxy.$1.port": "4444",
|
||||
"proxy.$2.port": "9999",
|
||||
"proxy.$3.scheme": "https",
|
||||
"proxy.#1.host": "localhost",
|
||||
"proxy.#1.port": "4444",
|
||||
"proxy.#2.port": "9999",
|
||||
"proxy.#3.port": "1111",
|
||||
"proxy.#3.scheme": "https",
|
||||
},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 3333, PublicPort: 9999},
|
||||
@@ -182,8 +175,8 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
"proxy.$1.host": "localhost",
|
||||
"proxy.$4.scheme": "https",
|
||||
"proxy.#1.host": "localhost",
|
||||
"proxy.#4.scheme": "https",
|
||||
}}, "")
|
||||
_, err := p.entriesFromContainerLabels(c)
|
||||
ExpectError(t, E.ErrOutOfRange, err.Error())
|
||||
@@ -193,7 +186,7 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
"proxy.$0.host": "localhost",
|
||||
"proxy.#0.host": "localhost",
|
||||
}}, ""))
|
||||
ExpectError(t, E.ErrOutOfRange, err.Error())
|
||||
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
||||
@@ -249,7 +242,9 @@ func TestImplicitExclude(t *testing.T) {
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
}}, ""))
|
||||
},
|
||||
State: "running",
|
||||
}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("a")
|
||||
@@ -264,6 +259,7 @@ func TestImplicitExcludeNoExposedPort(t *testing.T) {
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
},
|
||||
State: "running",
|
||||
}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
@@ -271,7 +267,7 @@ func TestImplicitExcludeNoExposedPort(t *testing.T) {
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestExcludeNonExposedPort(t *testing.T) {
|
||||
func TestNotExcludeSpecifiedPort(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Image: "redis",
|
||||
@@ -280,13 +276,13 @@ func TestExcludeNonExposedPort(t *testing.T) {
|
||||
{Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"proxy.redis.port": "6379:6379", // should be excluded even specified
|
||||
"proxy.redis.port": "6379:6379", // but specified in label
|
||||
},
|
||||
}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("redis")
|
||||
ExpectFalse(t, ok)
|
||||
ExpectTrue(t, ok)
|
||||
}
|
||||
|
||||
func TestNotExcludeNonExposedPortHostNetwork(t *testing.T) {
|
||||
@@ -298,7 +294,7 @@ func TestNotExcludeNonExposedPortHostNetwork(t *testing.T) {
|
||||
{Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"proxy.redis.port": "6379:6379", // should be excluded even specified
|
||||
"proxy.redis.port": "6379:6379",
|
||||
},
|
||||
}
|
||||
cont.HostConfig.NetworkMode = "host"
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/yusing/go-proxy/common"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
U "github.com/yusing/go-proxy/utils"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
M "github.com/yusing/go-proxy/internal/models"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
)
|
||||
|
||||
type FileProvider struct {
|
||||
@@ -94,5 +94,5 @@ func (p *FileProvider) LoadRoutesImpl() (routes R.Routes, res E.NestedError) {
|
||||
}
|
||||
|
||||
func (p *FileProvider) NewWatcher() W.Watcher {
|
||||
return W.NewFileWatcher(p.fileName)
|
||||
return W.NewConfigFileWatcher(p.fileName)
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"path"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -56,6 +56,9 @@ func newProvider(name string, t ProviderType) *Provider {
|
||||
|
||||
func NewFileProvider(filename string) (p *Provider, err E.NestedError) {
|
||||
name := path.Base(filename)
|
||||
if name == "" {
|
||||
return nil, E.Invalid("file name", "empty")
|
||||
}
|
||||
p = newProvider(name, ProviderTypeFile)
|
||||
p.ProviderImpl, err = FileProviderImpl(filename)
|
||||
if err != nil {
|
||||
@@ -66,8 +69,17 @@ func NewFileProvider(filename string) (p *Provider, err E.NestedError) {
|
||||
}
|
||||
|
||||
func NewDockerProvider(name string, dockerHost string) (p *Provider, err E.NestedError) {
|
||||
if name == "" {
|
||||
return nil, E.Invalid("provider name", "empty")
|
||||
}
|
||||
explicitOnly := false
|
||||
if name[len(name)-1] == '!' {
|
||||
explicitOnly = true
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
|
||||
p = newProvider(name, ProviderTypeDocker)
|
||||
p.ProviderImpl, err = DockerProviderImpl(dockerHost)
|
||||
p.ProviderImpl, err = DockerProviderImpl(dockerHost, explicitOnly)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/docker/idlewatcher"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
P "github.com/yusing/go-proxy/proxy"
|
||||
PT "github.com/yusing/go-proxy/proxy/fields"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/error_page"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/docker/idlewatcher"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
. "github.com/yusing/go-proxy/internal/http"
|
||||
P "github.com/yusing/go-proxy/internal/proxy"
|
||||
PT "github.com/yusing/go-proxy/internal/proxy/fields"
|
||||
"github.com/yusing/go-proxy/internal/route/middleware"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -26,7 +28,7 @@ type (
|
||||
|
||||
entry *P.ReverseProxyEntry
|
||||
mux *http.ServeMux
|
||||
handler *P.ReverseProxy
|
||||
handler *ReverseProxy
|
||||
|
||||
regIdleWatcher func() E.NestedError
|
||||
unregIdleWatcher func()
|
||||
@@ -36,18 +38,41 @@ type (
|
||||
SubdomainKey = PT.Alias
|
||||
)
|
||||
|
||||
var (
|
||||
findMuxFunc = findMuxAnyDomain
|
||||
|
||||
httpRoutes = F.NewMapOf[SubdomainKey, *HTTPRoute]()
|
||||
httpRoutesMu sync.Mutex
|
||||
globalMux = http.NewServeMux() // TODO: support regex subdomain matching
|
||||
)
|
||||
|
||||
func SetFindMuxDomains(domains []string) {
|
||||
if len(domains) == 0 {
|
||||
findMuxFunc = findMuxAnyDomain
|
||||
} else {
|
||||
findMuxFunc = findMuxByDomains(domains)
|
||||
}
|
||||
}
|
||||
|
||||
func NewHTTPRoute(entry *P.ReverseProxyEntry) (*HTTPRoute, E.NestedError) {
|
||||
var trans *http.Transport
|
||||
var regIdleWatcher func() E.NestedError
|
||||
var unregIdleWatcher func()
|
||||
|
||||
if entry.NoTLSVerify {
|
||||
trans = transportNoTLS.Clone()
|
||||
trans = common.DefaultTransportNoTLS.Clone()
|
||||
} else {
|
||||
trans = transport.Clone()
|
||||
trans = common.DefaultTransport.Clone()
|
||||
}
|
||||
|
||||
rp := P.NewReverseProxy(entry.URL, trans, entry)
|
||||
rp := NewReverseProxy(entry.URL, trans)
|
||||
|
||||
if len(entry.Middlewares) > 0 {
|
||||
err := middleware.PatchReverseProxy(rp, entry.Middlewares)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if entry.UseIdleWatcher() {
|
||||
// allow time for response header up to `WakeTimeout`
|
||||
@@ -74,7 +99,7 @@ func NewHTTPRoute(entry *P.ReverseProxyEntry) (*HTTPRoute, E.NestedError) {
|
||||
|
||||
_, exists := httpRoutes.Load(entry.Alias)
|
||||
if exists {
|
||||
return nil, E.AlreadyExist("HTTPRoute alias", entry.Alias)
|
||||
return nil, E.Duplicated("HTTPRoute alias", entry.Alias)
|
||||
}
|
||||
|
||||
r := &HTTPRoute{
|
||||
@@ -94,11 +119,16 @@ func (r *HTTPRoute) String() string {
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) Start() E.NestedError {
|
||||
if r.mux != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
httpRoutesMu.Lock()
|
||||
defer httpRoutesMu.Unlock()
|
||||
|
||||
if r.regIdleWatcher != nil {
|
||||
if err := r.regIdleWatcher(); err.HasError() {
|
||||
r.unregIdleWatcher = nil
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -113,6 +143,10 @@ func (r *HTTPRoute) Start() E.NestedError {
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) Stop() E.NestedError {
|
||||
if r.mux == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
httpRoutesMu.Lock()
|
||||
defer httpRoutesMu.Unlock()
|
||||
|
||||
@@ -135,43 +169,58 @@ func (u *URL) MarshalText() (text []byte, err error) {
|
||||
}
|
||||
|
||||
func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
mux, err := findMux(r.Host)
|
||||
mux, err := findMuxFunc(r.Host)
|
||||
if err != nil {
|
||||
err = E.Failure("request").
|
||||
Subjectf("%s %s%s", r.Method, r.Host, r.URL.Path).
|
||||
With(err)
|
||||
http.Error(w, err.String(), http.StatusNotFound)
|
||||
logrus.Error(err)
|
||||
if !middleware.ServeStaticErrorPageFile(w, r) {
|
||||
logrus.Error(E.Failure("request").
|
||||
Subjectf("%s %s", r.Method, r.URL.String()).
|
||||
With(err))
|
||||
errorPage, ok := error_page.GetErrorPageByStatus(http.StatusNotFound)
|
||||
if ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write(errorPage)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func findMux(host string) (*http.ServeMux, E.NestedError) {
|
||||
sd := strings.Split(host, ".")[0]
|
||||
func findMuxAnyDomain(host string) (*http.ServeMux, error) {
|
||||
hostSplit := strings.Split(host, ".")
|
||||
n := len(hostSplit)
|
||||
if n <= 2 {
|
||||
return nil, fmt.Errorf("missing subdomain in url")
|
||||
}
|
||||
sd := strings.Join(hostSplit[:n-2], ".")
|
||||
if r, ok := httpRoutes.Load(PT.Alias(sd)); ok {
|
||||
return r.mux, nil
|
||||
}
|
||||
return nil, E.NotExist("route", sd)
|
||||
return nil, fmt.Errorf("no such route: %s", sd)
|
||||
}
|
||||
|
||||
var (
|
||||
defaultDialer = net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
}
|
||||
transport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: defaultDialer.DialContext,
|
||||
MaxIdleConnsPerHost: 1000,
|
||||
}
|
||||
transportNoTLS = func() *http.Transport {
|
||||
var clone = transport.Clone()
|
||||
clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
return clone
|
||||
}()
|
||||
func findMuxByDomains(domains []string) func(host string) (*http.ServeMux, error) {
|
||||
return func(host string) (*http.ServeMux, error) {
|
||||
var subdomain string
|
||||
|
||||
httpRoutes = F.NewMapOf[SubdomainKey, *HTTPRoute]()
|
||||
httpRoutesMu sync.Mutex
|
||||
globalMux = http.NewServeMux()
|
||||
)
|
||||
for _, domain := range domains {
|
||||
if !strings.HasPrefix(domain, ".") {
|
||||
domain = "." + domain
|
||||
}
|
||||
subdomain = strings.TrimSuffix(host, domain)
|
||||
if len(subdomain) < len(host) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(subdomain) == len(host) { // not matched
|
||||
return nil, fmt.Errorf("%s does not match any base domain", host)
|
||||
}
|
||||
if r, ok := httpRoutes.Load(PT.Alias(subdomain)); ok {
|
||||
return r.mux, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no such route: %s", subdomain)
|
||||
}
|
||||
}
|
||||
75
internal/route/middleware/custom_error_page.go
Normal file
75
internal/route/middleware/custom_error_page.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/error_page"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
gpHTTP "github.com/yusing/go-proxy/internal/http"
|
||||
)
|
||||
|
||||
var CustomErrorPage = &Middleware{
|
||||
before: func(next http.Handler, w ResponseWriter, r *Request) {
|
||||
if !ServeStaticErrorPageFile(w, r) {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
},
|
||||
modifyResponse: func(resp *Response) error {
|
||||
// only handles non-success status code and html/plain content type
|
||||
contentType := gpHTTP.GetContentType(resp.Header)
|
||||
if !gpHTTP.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) {
|
||||
errorPage, ok := error_page.GetErrorPageByStatus(resp.StatusCode)
|
||||
if ok {
|
||||
errPageLogger.Debugf("error page for status %d loaded", resp.StatusCode)
|
||||
io.Copy(io.Discard, resp.Body) // drain the original body
|
||||
resp.Body.Close()
|
||||
resp.Body = io.NopCloser(bytes.NewReader(errorPage))
|
||||
resp.ContentLength = int64(len(errorPage))
|
||||
resp.Header.Set("Content-Length", fmt.Sprint(len(errorPage)))
|
||||
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
|
||||
} else {
|
||||
errPageLogger.Errorf("unable to load error page for status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func ServeStaticErrorPageFile(w http.ResponseWriter, r *http.Request) bool {
|
||||
path := r.URL.Path
|
||||
if path != "" && path[0] != '/' {
|
||||
path = "/" + path
|
||||
}
|
||||
if strings.HasPrefix(path, common.StaticFilePathPrefix) {
|
||||
filename := path[len(common.StaticFilePathPrefix):]
|
||||
file, ok := error_page.GetStaticFile(filename)
|
||||
if !ok {
|
||||
errPageLogger.Errorf("unable to load resource %s", filename)
|
||||
return false
|
||||
} else {
|
||||
ext := filepath.Ext(filename)
|
||||
switch ext {
|
||||
case ".html":
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
case ".js":
|
||||
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
||||
case ".css":
|
||||
w.Header().Set("Content-Type", "text/css; charset=utf-8")
|
||||
default:
|
||||
errPageLogger.Errorf("unexpected file type %q for %s", ext, filename)
|
||||
}
|
||||
w.Write(file)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var errPageLogger = logrus.WithField("middleware", "error_page")
|
||||
249
internal/route/middleware/forward_auth.go
Normal file
249
internal/route/middleware/forward_auth.go
Normal file
@@ -0,0 +1,249 @@
|
||||
// Modified from Traefik Labs's MIT-licensed code (https://github.com/traefik/traefik/blob/master/pkg/middlewares/auth/forward.go)
|
||||
// Copyright (c) 2020-2024 Traefik Labs
|
||||
// Copyright (c) 2024 yusing
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
gpHTTP "github.com/yusing/go-proxy/internal/http"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
forwardAuth struct {
|
||||
*forwardAuthOpts
|
||||
m *Middleware
|
||||
client http.Client
|
||||
}
|
||||
forwardAuthOpts struct {
|
||||
Address string
|
||||
TrustForwardHeader bool
|
||||
AuthResponseHeaders []string
|
||||
AddAuthCookiesToResponse []string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
xForwardedFor = "X-Forwarded-For"
|
||||
xForwardedMethod = "X-Forwarded-Method"
|
||||
xForwardedHost = "X-Forwarded-Host"
|
||||
xForwardedProto = "X-Forwarded-Proto"
|
||||
xForwardedURI = "X-Forwarded-Uri"
|
||||
xForwardedPort = "X-Forwarded-Port"
|
||||
)
|
||||
|
||||
var ForwardAuth = newForwardAuth()
|
||||
var faLogger = logrus.WithField("middleware", "ForwardAuth")
|
||||
|
||||
func newForwardAuth() (fa *forwardAuth) {
|
||||
fa = new(forwardAuth)
|
||||
fa.m = new(Middleware)
|
||||
fa.m.labelParserMap = D.ValueParserMap{
|
||||
"trust_forward_header": D.BoolParser,
|
||||
"auth_response_headers": D.YamlStringListParser,
|
||||
"add_auth_cookies_to_response": D.YamlStringListParser,
|
||||
}
|
||||
fa.m.withOptions = func(optsRaw OptionsRaw, rp *ReverseProxy) (*Middleware, E.NestedError) {
|
||||
tr, ok := rp.Transport.(*http.Transport)
|
||||
if ok {
|
||||
tr = tr.Clone()
|
||||
} else {
|
||||
tr = common.DefaultTransport.Clone()
|
||||
}
|
||||
|
||||
faWithOpts := new(forwardAuth)
|
||||
faWithOpts.forwardAuthOpts = new(forwardAuthOpts)
|
||||
faWithOpts.client = http.Client{
|
||||
CheckRedirect: func(r *Request, via []*Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: tr,
|
||||
}
|
||||
faWithOpts.m = &Middleware{
|
||||
impl: faWithOpts,
|
||||
before: faWithOpts.forward,
|
||||
}
|
||||
|
||||
err := U.Deserialize(optsRaw, faWithOpts.forwardAuthOpts)
|
||||
if err != nil {
|
||||
return nil, E.FailWith("set options", err)
|
||||
}
|
||||
_, err = E.Check(url.Parse(faWithOpts.Address))
|
||||
if err != nil {
|
||||
return nil, E.Invalid("address", faWithOpts.Address)
|
||||
}
|
||||
return faWithOpts.m, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (fa *forwardAuth) forward(next http.Handler, w ResponseWriter, req *Request) {
|
||||
gpHTTP.RemoveHop(req.Header)
|
||||
|
||||
faReq, err := http.NewRequestWithContext(
|
||||
req.Context(),
|
||||
http.MethodGet,
|
||||
fa.Address,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
faLogger.Debugf("new request err to %s: %s", fa.Address, err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
gpHTTP.CopyHeader(faReq.Header, req.Header)
|
||||
gpHTTP.RemoveHop(faReq.Header)
|
||||
|
||||
gpHTTP.FilterHeaders(faReq.Header, fa.AuthResponseHeaders)
|
||||
fa.setAuthHeaders(req, faReq)
|
||||
|
||||
faResp, err := fa.client.Do(faReq)
|
||||
if err != nil {
|
||||
faLogger.Debugf("failed to call %s: %s", fa.Address, err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer faResp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(faResp.Body)
|
||||
if err != nil {
|
||||
faLogger.Debugf("failed to read response body from %s: %s", fa.Address, err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if faResp.StatusCode < http.StatusOK || faResp.StatusCode >= http.StatusMultipleChoices {
|
||||
gpHTTP.CopyHeader(w.Header(), faResp.Header)
|
||||
gpHTTP.RemoveHop(w.Header())
|
||||
|
||||
redirectURL, err := faResp.Location()
|
||||
if err != nil {
|
||||
faLogger.Debugf("failed to get location from %s: %s", fa.Address, err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
} else if redirectURL.String() != "" {
|
||||
w.Header().Set("Location", redirectURL.String())
|
||||
}
|
||||
|
||||
w.WriteHeader(faResp.StatusCode)
|
||||
|
||||
if _, err = w.Write(body); err != nil {
|
||||
faLogger.Debugf("failed to write response body from %s: %s", fa.Address, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, key := range fa.AuthResponseHeaders {
|
||||
key := http.CanonicalHeaderKey(key)
|
||||
req.Header.Del(key)
|
||||
if len(faResp.Header[key]) > 0 {
|
||||
req.Header[key] = append([]string(nil), faResp.Header[key]...)
|
||||
}
|
||||
}
|
||||
|
||||
req.RequestURI = req.URL.RequestURI()
|
||||
|
||||
authCookies := faResp.Cookies()
|
||||
|
||||
if len(authCookies) == 0 {
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(gpHTTP.NewModifyResponseWriter(w, req, func(resp *Response) error {
|
||||
fa.setAuthCookies(resp, authCookies)
|
||||
return nil
|
||||
}), req)
|
||||
}
|
||||
|
||||
func (fa *forwardAuth) setAuthCookies(resp *Response, authCookies []*Cookie) {
|
||||
if len(fa.AddAuthCookiesToResponse) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cookies := resp.Cookies()
|
||||
resp.Header.Del("Set-Cookie")
|
||||
|
||||
for _, cookie := range cookies {
|
||||
if !slices.Contains(fa.AddAuthCookiesToResponse, cookie.Name) {
|
||||
// this cookie is not an auth cookie, so add it back
|
||||
resp.Header.Add("Set-Cookie", cookie.String())
|
||||
}
|
||||
}
|
||||
|
||||
for _, cookie := range authCookies {
|
||||
if slices.Contains(fa.AddAuthCookiesToResponse, cookie.Name) {
|
||||
// this cookie is an auth cookie, so add to resp
|
||||
resp.Header.Add("Set-Cookie", cookie.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fa *forwardAuth) setAuthHeaders(req, faReq *Request) {
|
||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
if fa.TrustForwardHeader {
|
||||
if prior, ok := req.Header[xForwardedFor]; ok {
|
||||
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
||||
}
|
||||
}
|
||||
faReq.Header.Set(xForwardedFor, clientIP)
|
||||
}
|
||||
|
||||
xMethod := req.Header.Get(xForwardedMethod)
|
||||
switch {
|
||||
case xMethod != "" && fa.TrustForwardHeader:
|
||||
faReq.Header.Set(xForwardedMethod, xMethod)
|
||||
case req.Method != "":
|
||||
faReq.Header.Set(xForwardedMethod, req.Method)
|
||||
default:
|
||||
faReq.Header.Del(xForwardedMethod)
|
||||
}
|
||||
|
||||
xfp := req.Header.Get(xForwardedProto)
|
||||
switch {
|
||||
case xfp != "" && fa.TrustForwardHeader:
|
||||
faReq.Header.Set(xForwardedProto, xfp)
|
||||
case req.TLS != nil:
|
||||
faReq.Header.Set(xForwardedProto, "https")
|
||||
default:
|
||||
faReq.Header.Set(xForwardedProto, "http")
|
||||
}
|
||||
|
||||
if xfp := req.Header.Get(xForwardedPort); xfp != "" && fa.TrustForwardHeader {
|
||||
faReq.Header.Set(xForwardedPort, xfp)
|
||||
}
|
||||
|
||||
xfh := req.Header.Get(xForwardedHost)
|
||||
switch {
|
||||
case xfh != "" && fa.TrustForwardHeader:
|
||||
faReq.Header.Set(xForwardedHost, xfh)
|
||||
case req.Host != "":
|
||||
faReq.Header.Set(xForwardedHost, req.Host)
|
||||
default:
|
||||
faReq.Header.Del(xForwardedHost)
|
||||
}
|
||||
|
||||
xfURI := req.Header.Get(xForwardedURI)
|
||||
switch {
|
||||
case xfURI != "" && fa.TrustForwardHeader:
|
||||
faReq.Header.Set(xForwardedURI, xfURI)
|
||||
case req.URL.RequestURI() != "":
|
||||
faReq.Header.Set(xForwardedURI, req.URL.RequestURI())
|
||||
default:
|
||||
faReq.Header.Del(xForwardedURI)
|
||||
}
|
||||
}
|
||||
145
internal/route/middleware/middleware.go
Normal file
145
internal/route/middleware/middleware.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
gpHTTP "github.com/yusing/go-proxy/internal/http"
|
||||
)
|
||||
|
||||
type (
|
||||
Error = E.NestedError
|
||||
|
||||
ReverseProxy = gpHTTP.ReverseProxy
|
||||
ProxyRequest = gpHTTP.ProxyRequest
|
||||
Request = http.Request
|
||||
Response = http.Response
|
||||
ResponseWriter = http.ResponseWriter
|
||||
Header = http.Header
|
||||
Cookie = http.Cookie
|
||||
|
||||
BeforeFunc func(next http.Handler, w ResponseWriter, r *Request)
|
||||
RewriteFunc func(req *ProxyRequest)
|
||||
ModifyResponseFunc func(resp *Response) error
|
||||
CloneWithOptFunc func(opts OptionsRaw, rp *ReverseProxy) (*Middleware, E.NestedError)
|
||||
|
||||
OptionsRaw = map[string]any
|
||||
Options any
|
||||
|
||||
Middleware struct {
|
||||
name string
|
||||
|
||||
before BeforeFunc // runs before ReverseProxy.ServeHTTP
|
||||
rewrite RewriteFunc // runs after ReverseProxy.Rewrite
|
||||
modifyResponse ModifyResponseFunc // runs after ReverseProxy.ModifyResponse
|
||||
|
||||
transport http.RoundTripper
|
||||
|
||||
withOptions CloneWithOptFunc
|
||||
labelParserMap D.ValueParserMap
|
||||
impl any
|
||||
}
|
||||
)
|
||||
|
||||
func (m *Middleware) Name() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
func (m *Middleware) String() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
func (m *Middleware) WithOptionsClone(optsRaw OptionsRaw, rp *ReverseProxy) (*Middleware, E.NestedError) {
|
||||
if len(optsRaw) != 0 && m.withOptions != nil {
|
||||
if mWithOpt, err := m.withOptions(optsRaw, rp); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return mWithOpt, nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithOptionsClone is called only once
|
||||
// set withOptions and labelParser will not be used after that
|
||||
return &Middleware{m.name, m.before, m.rewrite, m.modifyResponse, m.transport, nil, nil, m.impl}, nil
|
||||
}
|
||||
|
||||
// TODO: check conflict or duplicates
|
||||
func PatchReverseProxy(rp *ReverseProxy, middlewares map[string]OptionsRaw) (res E.NestedError) {
|
||||
befores := make([]BeforeFunc, 0, len(middlewares))
|
||||
rewrites := make([]RewriteFunc, 0, len(middlewares))
|
||||
modResps := make([]ModifyResponseFunc, 0, len(middlewares))
|
||||
|
||||
invalidM := E.NewBuilder("invalid middlewares")
|
||||
invalidOpts := E.NewBuilder("invalid options")
|
||||
defer func() {
|
||||
invalidM.Add(invalidOpts.Build())
|
||||
invalidM.To(&res)
|
||||
}()
|
||||
|
||||
for name, opts := range middlewares {
|
||||
m, ok := Get(name)
|
||||
if !ok {
|
||||
invalidM.Addf("%s", name)
|
||||
continue
|
||||
}
|
||||
|
||||
m, err := m.WithOptionsClone(opts, rp)
|
||||
if err != nil {
|
||||
invalidOpts.Add(err.Subject(name))
|
||||
continue
|
||||
}
|
||||
if m.before != nil {
|
||||
befores = append(befores, m.before)
|
||||
}
|
||||
if m.rewrite != nil {
|
||||
rewrites = append(rewrites, m.rewrite)
|
||||
}
|
||||
if m.modifyResponse != nil {
|
||||
modResps = append(modResps, m.modifyResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if invalidM.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
origServeHTTP := rp.ServeHTTP
|
||||
for i, before := range befores {
|
||||
if i < len(befores)-1 {
|
||||
rp.ServeHTTP = func(w ResponseWriter, r *Request) {
|
||||
before(rp.ServeHTTP, w, r)
|
||||
}
|
||||
} else {
|
||||
rp.ServeHTTP = func(w ResponseWriter, r *Request) {
|
||||
before(origServeHTTP, w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(rewrites) > 0 {
|
||||
if rp.Rewrite != nil {
|
||||
rewrites = append([]RewriteFunc{rp.Rewrite}, rewrites...)
|
||||
}
|
||||
rp.Rewrite = func(req *ProxyRequest) {
|
||||
for _, rewrite := range rewrites {
|
||||
rewrite(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(modResps) > 0 {
|
||||
if rp.ModifyResponse != nil {
|
||||
modResps = append([]ModifyResponseFunc{rp.ModifyResponse}, modResps...)
|
||||
}
|
||||
rp.ModifyResponse = func(res *Response) error {
|
||||
b := E.NewBuilder("errors in middleware ModifyResponse")
|
||||
for _, mr := range modResps {
|
||||
b.AddE(mr(res))
|
||||
}
|
||||
return b.Build().Error()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
45
internal/route/middleware/middlewares.go
Normal file
45
internal/route/middleware/middlewares.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
)
|
||||
|
||||
var middlewares map[string]*Middleware
|
||||
|
||||
func Get(name string) (middleware *Middleware, ok bool) {
|
||||
middleware, ok = middlewares[name]
|
||||
return
|
||||
}
|
||||
|
||||
// initialize middleware names and label parsers
|
||||
func init() {
|
||||
middlewares = map[string]*Middleware{
|
||||
"set_x_forwarded": SetXForwarded,
|
||||
"add_x_forwarded": AddXForwarded,
|
||||
"redirect_http": RedirectHTTP,
|
||||
"forward_auth": ForwardAuth.m,
|
||||
"modify_response": ModifyResponse.m,
|
||||
"modify_request": ModifyRequest.m,
|
||||
"error_page": CustomErrorPage,
|
||||
"custom_error_page": CustomErrorPage,
|
||||
}
|
||||
names := make(map[*Middleware][]string)
|
||||
for name, m := range middlewares {
|
||||
names[m] = append(names[m], name)
|
||||
// register middleware name to docker label parsr
|
||||
// in order to parse middleware_name.option=value into correct type
|
||||
if m.labelParserMap != nil {
|
||||
D.RegisterNamespace(name, m.labelParserMap)
|
||||
}
|
||||
}
|
||||
for m, names := range names {
|
||||
if len(names) > 1 {
|
||||
m.name = fmt.Sprintf("%s (a.k.a. %s)", names[0], strings.Join(names[1:], ", "))
|
||||
} else {
|
||||
m.name = names[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user