mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-14 06:13:33 +01:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1c9e18c97 | ||
|
|
ef83ed0596 | ||
|
|
d89155a6ee | ||
|
|
921ce23dde | ||
|
|
929b7f7059 | ||
|
|
de7805f281 | ||
|
|
03cad9f315 | ||
|
|
aa6fafd52f | ||
|
|
01ff63a007 | ||
|
|
99746bad8e | ||
|
|
21b67e97af | ||
|
|
668639e484 | ||
|
|
e9b2079599 | ||
|
|
5fb7d21c80 | ||
|
|
f5e00a6ef4 | ||
|
|
b06cbc0fee | ||
|
|
abbcbad5e9 | ||
|
|
fab39a461f | ||
|
|
9c3edff92b | ||
|
|
e8f4cd18a4 | ||
|
|
e566fd9b57 | ||
|
|
6211ddcdf0 | ||
|
|
245f073350 | ||
|
|
dd629f516b | ||
|
|
31080edd59 | ||
|
|
b679655cd5 | ||
|
|
ca3b062f89 | ||
|
|
de6c1be51b | ||
|
|
4f09dbf044 | ||
|
|
e6b4630ce9 | ||
|
|
90bababd38 | ||
|
|
90130411f9 | ||
|
|
ae61a2335d | ||
|
|
8329a8ea9c | ||
|
|
ef52ccb929 | ||
|
|
ed9d8aab6f | ||
|
|
aa16287447 | ||
|
|
a7a922308e | ||
|
|
ba13b81b0e | ||
|
|
d172552fb0 | ||
|
|
2a8ab27fc1 | ||
|
|
e8c3e4c75f | ||
|
|
ed887a5cfc | ||
|
|
1bac96dc2a | ||
|
|
c3b779a810 | ||
|
|
44cfd65f6c | ||
|
|
f5a36f94bb | ||
|
|
e951194bee | ||
|
|
478311fe9e | ||
|
|
48dd1397e8 | ||
|
|
ebedbc931f | ||
|
|
9065d990e5 | ||
|
|
b38d7595a7 | ||
|
|
860e914b90 | ||
|
|
ac3af49aa7 |
16
.github/workflows/docker-image.yml
vendored
16
.github/workflows/docker-image.yml
vendored
@@ -11,7 +11,7 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
name: Build multi-platform Docker image
|
||||
runs-on: self-hosted
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -24,8 +24,8 @@ jobs:
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
# - linux/arm/v6
|
||||
# - linux/arm/v7
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
@@ -61,6 +61,8 @@ jobs:
|
||||
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
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
@@ -83,7 +85,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
merge:
|
||||
runs-on: self-hosted
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build
|
||||
permissions:
|
||||
@@ -124,9 +126,3 @@ jobs:
|
||||
- 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
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
compose.yml
|
||||
*.compose.yml
|
||||
|
||||
config*/
|
||||
certs*/
|
||||
@@ -19,3 +20,5 @@ todo.md
|
||||
|
||||
.*.swp
|
||||
.aider*
|
||||
mtrace.json
|
||||
.env
|
||||
|
||||
@@ -11,5 +11,5 @@ build-image:
|
||||
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
|
||||
script:
|
||||
- echo building $CI_REGISTRY_IMAGE
|
||||
- docker build --pull -t $CI_REGISTRY_IMAGE .
|
||||
- docker push $CI_REGISTRY_IMAGE
|
||||
- docker build --no-cache --build-arg VERSION=$CI_COMMIT_REF_NAME -t $CI_REGISTRY_IMAGE .
|
||||
- docker push $CI_REGISTRY_IMAGE
|
||||
|
||||
6
.vscode/settings.example.json
vendored
6
.vscode/settings.example.json
vendored
@@ -5,9 +5,7 @@
|
||||
"config.yml"
|
||||
],
|
||||
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
|
||||
"providers.example.yml",
|
||||
"*.providers.yml",
|
||||
"providers.yml"
|
||||
"providers.example.yml"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Dockerfile
21
Dockerfile
@@ -1,25 +1,32 @@
|
||||
# Stage 1: Builder
|
||||
FROM golang:1.23.1-alpine AS builder
|
||||
RUN apk add --no-cache tzdata
|
||||
FROM golang:1.23.2-alpine AS builder
|
||||
RUN apk add --no-cache tzdata make
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Only copy go.mod and go.sum initially for better caching
|
||||
COPY go.mod go.sum /src
|
||||
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
|
||||
go mod download -x
|
||||
|
||||
ENV GOCACHE=/root/.cache/go-build
|
||||
|
||||
# Build the application with better caching
|
||||
ARG VERSION
|
||||
ENV VERSION=${VERSION}
|
||||
|
||||
COPY scripts /src/scripts
|
||||
COPY Makefile /src/
|
||||
|
||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||
--mount=type=cache,target="/root/.cache/go-build" \
|
||||
--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
|
||||
--mount=type=bind,src=pkg,dst=/src/pkg \
|
||||
make build && \
|
||||
mkdir -p /app/error_pages /app/certs && \
|
||||
mv bin/go-proxy /app/go-proxy
|
||||
|
||||
# Stage 2: Final image
|
||||
FROM scratch
|
||||
|
||||
28
Makefile
28
Makefile
@@ -1,21 +1,19 @@
|
||||
BUILD_FLAG ?= -s -w
|
||||
VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||
BUILD_FLAGS ?= -s -w -X github.com/yusing/go-proxy/pkg.version=${VERSION}
|
||||
export VERSION
|
||||
export BUILD_FLAGS
|
||||
export CGO_ENABLED = 0
|
||||
export GOOS = linux
|
||||
|
||||
.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
|
||||
[ -f config/config.yml ] || cp config.example.yml config/config.yml
|
||||
[ -f config/providers.yml ] || touch config/providers.yml
|
||||
|
||||
build:
|
||||
mkdir -p bin
|
||||
CGO_ENABLED=0 GOOS=linux \
|
||||
go build -ldflags '${BUILD_FLAG}' -pgo=auto -o bin/go-proxy ./cmd
|
||||
scripts/build.sh
|
||||
|
||||
test:
|
||||
go test ./internal/...
|
||||
GOPROXY_TEST=1 go test ./internal/...
|
||||
|
||||
up:
|
||||
docker compose up -d
|
||||
@@ -27,10 +25,16 @@ logs:
|
||||
docker compose logs -f
|
||||
|
||||
get:
|
||||
cd cmd && go get -u && go mod tidy && cd ..
|
||||
go get -u ./cmd && go mod tidy
|
||||
|
||||
debug:
|
||||
make BUILD_FLAG="" build && sudo GOPROXY_DEBUG=1 bin/go-proxy
|
||||
make build && sudo GOPROXY_DEBUG=1 bin/go-proxy
|
||||
|
||||
mtrace:
|
||||
bin/go-proxy debug-ls-mtrace > mtrace.json
|
||||
|
||||
run-test:
|
||||
make build && sudo GOPROXY_TEST=1 bin/go-proxy
|
||||
|
||||
run:
|
||||
make build && sudo bin/go-proxy
|
||||
|
||||
83
README.md
83
README.md
@@ -9,7 +9,11 @@
|
||||
|
||||
[繁體中文文檔請看此](README_CHT.md)
|
||||
|
||||
A lightweight, easy-to-use, and [performant](docs/benchmark_result.md) reverse proxy with a web UI.
|
||||
A lightweight, easy-to-use, and [performant](https://github.com/yusing/go-proxy/wiki/Benchmarks) reverse proxy with a Web UI and dashboard.
|
||||
|
||||

|
||||
|
||||
_Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
|
||||
## Table of content
|
||||
|
||||
@@ -17,34 +21,30 @@ A lightweight, easy-to-use, and [performant](docs/benchmark_result.md) reverse p
|
||||
|
||||
- [go-proxy](#go-proxy)
|
||||
- [Table of content](#table-of-content)
|
||||
- [Key Points](#key-points)
|
||||
- [Key Features](#key-features)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Setup](#setup)
|
||||
- [Commands line arguments](#commands-line-arguments)
|
||||
- [Environment variables](#environment-variables)
|
||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
||||
- [Config File](#config-file)
|
||||
- [Include Files](#include-files)
|
||||
- [Showcase](#showcase)
|
||||
- [Screenshots](#screenshots)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
- [Build it yourself](#build-it-yourself)
|
||||
|
||||
## Key Points
|
||||
## Key Features
|
||||
|
||||
- Easy to use
|
||||
- Effortless configuration
|
||||
- Simple multi-node setup
|
||||
- Error messages is clear and detailed, easy troubleshooting
|
||||
- Auto SSL cert management (See [Supported DNS Challenge Providers](docs/dns_providers.md))
|
||||
- Auto SSL cert management (See [Supported DNS-01 Challenge Providers](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
||||
- Auto configuration for docker containers
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- **idlesleeper**: stop containers on idle, wake it up on traffic _(optional, see [showcase](#idlesleeper))_
|
||||
- **idlesleeper**: stop containers on idle, wake it up on traffic _(optional, see [screenshots](#idlesleeper))_
|
||||
- HTTP(s) reserve proxy
|
||||
- [HTTP middleware support](docs/middlewares.md) _(experimental)_
|
||||
- [Custom error pages support](docs/middlewares.md#custom-error-pages)
|
||||
- [HTTP middleware support](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||
- [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#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
|
||||
- **Web UI with App dashboard**
|
||||
- Supports linux/amd64, linux/arm64
|
||||
- Written in **[Go](https://go.dev)**
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
@@ -70,64 +70,31 @@ A lightweight, easy-to-use, and [performant](docs/benchmark_result.md) reverse p
|
||||
- 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`
|
||||
4. Setup `docker-socket-proxy` other docker nodes _(if any)_ (see [Multi docker nodes setup](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)) and then them inside `config.yml`
|
||||
|
||||
5. Done. You may now do some extra configuration
|
||||
5. Run go-proxy `docker compose up -d`
|
||||
then list all routes to see if further configurations are needed:
|
||||
`docker exec go-proxy /app/go-proxy ls-routes`
|
||||
|
||||
6. 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)
|
||||
- With Web UI via `http://localhost:3000` or `https://gp.y.z`
|
||||
- For more info, [See Wiki]([wiki](https://github.com/yusing/go-proxy/wiki))
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Commands line arguments
|
||||
|
||||
| Argument | Description | Example |
|
||||
| ----------- | -------------------------------- | -------------------------- |
|
||||
| empty | start proxy server | |
|
||||
| `validate` | validate config and exit | |
|
||||
| `reload` | trigger a force reload of config | |
|
||||
| `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 go-proxy /app/go-proxy <command>`**
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Environment Variable | Description | Default | Values |
|
||||
| ------------------------------ | ------------------------------------------- | ---------------- | ------------- |
|
||||
| `GOPROXY_NO_SCHEMA_VALIDATION` | disable schema validation | `false` | boolean |
|
||||
| `GOPROXY_DEBUG` | enable debug behaviors | `false` | boolean |
|
||||
| `GOPROXY_HTTP_ADDR` | http server listening address | `:80` | `[host]:port` |
|
||||
| `GOPROXY_HTTPS_ADDR` | https server listening address (if enabled) | `:443` | `[host]:port` |
|
||||
| `GOPROXY_API_ADDR` | api server listening address | `127.0.0.1:8888` | `[host]:port` |
|
||||
|
||||
### Use JSON Schema in VSCode
|
||||
|
||||
Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscode/settings.json` and modify it to fit your needs
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Config File
|
||||
|
||||
See [config.example.yml](config.example.yml)
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Include Files
|
||||
|
||||
These are files that include standalone proxy entries
|
||||
|
||||
See [Fields](docs/docker.md#fields)
|
||||
|
||||
See [providers.example.yml](providers.example.yml) for examples
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Showcase
|
||||
## Screenshots
|
||||
|
||||
### idlesleeper
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
一個輕量化、易用且[高效](docs/benchmark_result.md)的反向代理和端口轉發工具
|
||||
一個輕量化、易用且[高效]([docs/benchmark_result.md](https://github.com/yusing/go-proxy/wiki/Benchmarks)))的反向代理和端口轉發工具
|
||||
|
||||
## 目錄
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
- [命令行參數](#命令行參數)
|
||||
- [環境變量](#環境變量)
|
||||
- [VSCode 中使用 JSON Schema](#vscode-中使用-json-schema)
|
||||
- [配置文件](#配置文件)
|
||||
- [透過文件配置](#透過文件配置)
|
||||
- [展示](#展示)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
- [源碼編譯](#源碼編譯)
|
||||
@@ -33,14 +31,16 @@
|
||||
- 不需花費太多時間就能輕鬆配置
|
||||
- 支持多個docker節點
|
||||
- 除錯簡單
|
||||
- 自動配置 SSL 證書(參見[可用的 DNS 供應商](docs/dns_providers.md))
|
||||
- 自動配置 SSL 證書(參見[可用的 DNS 供應商](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
||||
- 透過 Docker 容器自動配置
|
||||
- 容器狀態變更時自動熱重載
|
||||
- 容器閒置時自動暫停/停止,入站時自動喚醒
|
||||
- **idlesleeper** 容器閒置時自動暫停/停止,入站時自動喚醒 (可選, 參見 [展示](#idlesleeper))
|
||||
- HTTP(s) 反向代理
|
||||
- [HTTP middleware](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||
- [自訂 error pages](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||
- 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 多平台
|
||||
- Web 面板 (內置App dashboard)
|
||||
- 支持 linux/amd64、linux/arm64 平台
|
||||
- 使用 **[Go](https://go.dev)** 編寫
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
@@ -70,20 +70,23 @@
|
||||
|
||||
5. 大功告成,你可以做一些額外的配置
|
||||
- 使用文本編輯器 (推薦 Visual Studio Code [參見 VSCode 使用 schema](#vscode-中使用-json-schema))
|
||||
- 或通過 `http://gp.y.z` 使用網頁配置編輯器
|
||||
- 或通過 `http://localhost:3000` 使用網頁配置編輯器
|
||||
- 詳情請參閱 [docker.md](docs/docker.md)
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
### 命令行參數
|
||||
|
||||
| 參數 | 描述 | 示例 |
|
||||
| ----------- | -------------- | -------------------------- |
|
||||
| 空 | 啟動代理服務器 | |
|
||||
| `validate` | 驗證配置並退出 | |
|
||||
| `reload` | 強制刷新配置 | |
|
||||
| `ls-config` | 列出配置並退出 | `go-proxy ls-config \| jq` |
|
||||
| `ls-route` | 列出路由並退出 | `go-proxy ls-route \| jq` |
|
||||
| 參數 | 描述 | 示例 |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------- |
|
||||
| 空 | 啟動代理服務器 | |
|
||||
| `validate` | 驗證配置並退出 | |
|
||||
| `reload` | 強制刷新配置 | |
|
||||
| `ls-config` | 列出配置並退出 | `go-proxy ls-config \| jq` |
|
||||
| `ls-route` | 列出路由並退出 | `go-proxy ls-route \| jq` |
|
||||
| `go-proxy ls-route \| jq` |
|
||||
| `ls-icons` | 列出 [dashboard-icons](https://github.com/walkxcode/dashboard-icons/tree/main) 並退出 | `go-proxy ls-icons \| grep adguard` |
|
||||
| `debug-ls-mtrace` | 列出middleware追蹤 **(僅限於 debug 模式)** | `go-proxy debug-ls-mtrace \| jq` |
|
||||
|
||||
**使用 `docker exec go-proxy /app/go-proxy <參數>` 運行**
|
||||
|
||||
@@ -103,25 +106,12 @@
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
### 配置文件
|
||||
|
||||
參見 [config.example.yml](config.example.yml)
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
### 透過文件配置
|
||||
|
||||
參見 [Fields](docs/docker.md#fields)
|
||||
|
||||
參見範例 [providers.example.yml](providers.example.yml)
|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
## 展示
|
||||
|
||||
### idlesleeper
|
||||
|
||||

|
||||

|
||||
|
||||
[🔼 返回頂部](#目錄)
|
||||
|
||||
|
||||
47
cmd/main.go
47
cmd/main.go
@@ -18,15 +18,17 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"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/api/v1/query"
|
||||
"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"
|
||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/server"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
"github.com/yusing/go-proxy/pkg"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -53,10 +55,11 @@ func main() {
|
||||
ForceColors: true,
|
||||
TimestampFormat: "01-02 15:04:05",
|
||||
})
|
||||
logrus.Infof("go-proxy version %s", pkg.GetVersion())
|
||||
}
|
||||
|
||||
if args.Command == common.CommandReload {
|
||||
if err := apiUtils.ReloadServer(); err.HasError() {
|
||||
if err := query.ReloadServer(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Print("ok")
|
||||
@@ -80,8 +83,9 @@ func main() {
|
||||
prepareDirectory(dir)
|
||||
}
|
||||
|
||||
err := config.Load()
|
||||
if err != nil {
|
||||
middleware.LoadComposeFiles()
|
||||
|
||||
if err := config.Load(); err != nil {
|
||||
logrus.Warn(err)
|
||||
}
|
||||
cfg := config.GetInstance()
|
||||
@@ -91,7 +95,21 @@ func main() {
|
||||
printJSON(cfg.Value())
|
||||
return
|
||||
case common.CommandListRoutes:
|
||||
printJSON(cfg.RoutesByAlias())
|
||||
routes, err := query.ListRoutes()
|
||||
if err != nil {
|
||||
log.Printf("failed to connect to api server: %s", err)
|
||||
log.Printf("falling back to config file")
|
||||
printJSON(cfg.RoutesByAlias())
|
||||
} else {
|
||||
printJSON(routes)
|
||||
}
|
||||
return
|
||||
case common.CommandListIcons:
|
||||
icons, err := internal.ListAvailableIcons()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
printJSON(icons)
|
||||
return
|
||||
case common.CommandDebugListEntries:
|
||||
printJSON(cfg.DumpEntries())
|
||||
@@ -99,18 +117,15 @@ func main() {
|
||||
case common.CommandDebugListProviders:
|
||||
printJSON(cfg.DumpProviders())
|
||||
return
|
||||
}
|
||||
|
||||
if common.IsDebug {
|
||||
printJSON(docker.GetRegisteredNamespaces())
|
||||
case common.CommandDebugListMTrace:
|
||||
trace, err := query.ListMiddlewareTraces()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
printJSON(trace)
|
||||
}
|
||||
|
||||
cfg.StartProxyProviders()
|
||||
|
||||
if err.HasError() {
|
||||
l.Warn(err)
|
||||
}
|
||||
|
||||
cfg.WatchChanges()
|
||||
|
||||
onShutdown.Add(docker.CloseAllClients)
|
||||
@@ -125,7 +140,7 @@ func main() {
|
||||
|
||||
if autocert != nil {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
if err = autocert.Setup(ctx); err != nil {
|
||||
if err := autocert.Setup(ctx); err != nil {
|
||||
l.Fatal(err)
|
||||
} else {
|
||||
onShutdown.Add(cancel)
|
||||
@@ -207,7 +222,7 @@ func funcName(f func()) string {
|
||||
|
||||
func printJSON(obj any) {
|
||||
j, err := E.Check(json.MarshalIndent(obj, "", " "))
|
||||
if err.HasError() {
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
rawLogger := log.New(os.Stdout, "", 0)
|
||||
|
||||
@@ -1,33 +1,45 @@
|
||||
services:
|
||||
frontend:
|
||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
||||
container_name: go-proxy-frontend
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
labels:
|
||||
- proxy.aliases=gp
|
||||
- proxy.gp.port=3000
|
||||
depends_on:
|
||||
- app
|
||||
app:
|
||||
image: ghcr.io/yusing/go-proxy:latest
|
||||
container_name: go-proxy
|
||||
restart: always
|
||||
network_mode: host
|
||||
environment:
|
||||
# (Optional) change this to your timezone to get correct log timestamp
|
||||
TZ: ETC/UTC
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./config:/app/config
|
||||
frontend:
|
||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
||||
container_name: go-proxy-frontend
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
depends_on:
|
||||
- app
|
||||
# if you also want to proxy the WebUI and access it via gp.y.z
|
||||
# labels:
|
||||
# - proxy.aliases=gp
|
||||
# - proxy.gp.port=3000
|
||||
|
||||
# (Optional) choose one of below to enable https
|
||||
# 1. use existing certificate
|
||||
# if your cert is not named `cert.crt` change `cert_path` in `config/config.yml`
|
||||
# if your cert key is not named `priv.key` change `key_path` in `config/config.yml`
|
||||
# Make sure the value is same as `GOPROXY_API_ADDR` below (if you have changed it)
|
||||
#
|
||||
# environment:
|
||||
# GOPROXY_API_ADDR: 127.0.0.1:8888
|
||||
app:
|
||||
image: ghcr.io/yusing/go-proxy:latest
|
||||
container_name: go-proxy
|
||||
restart: always
|
||||
network_mode: host
|
||||
environment:
|
||||
# (Optional) change this to your timezone to get correct log timestamp
|
||||
TZ: ETC/UTC
|
||||
|
||||
# - /path/to/certs:/app/certs
|
||||
# Change these if you need
|
||||
#
|
||||
# GOPROXY_HTTP_ADDR: :80
|
||||
# GOPROXY_HTTPS_ADDR: :443
|
||||
# GOPROXY_API_ADDR: 127.0.0.1:8888
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./config:/app/config
|
||||
|
||||
# 2. use autocert, certs will be stored in ./certs (or other path you specify)
|
||||
# (Optional) choose one of below to enable https
|
||||
# 1. use existing certificate
|
||||
# if your cert is not named `cert.crt` change `cert_path` in `config/config.yml`
|
||||
# if your cert key is not named `priv.key` change `key_path` in `config/config.yml`
|
||||
|
||||
# - ./certs:/app/certs
|
||||
# - /path/to/certs:/app/certs
|
||||
|
||||
# 2. use autocert, certs will be stored in ./certs (or other path you specify)
|
||||
|
||||
# - ./certs:/app/certs
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# Adding provider support
|
||||
|
||||
## **CloudDNS** as an example
|
||||
|
||||
1. Fork this repo, modify [autocert.go](../src/go-proxy/autocert.go#L305)
|
||||
|
||||
```go
|
||||
var providersGenMap = map[string]ProviderGenerator{
|
||||
"cloudflare": providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
||||
// add here, e.g.
|
||||
"clouddns": providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
||||
}
|
||||
```
|
||||
|
||||
2. Go to [https://go-acme.github.io/lego/dns/clouddns](https://go-acme.github.io/lego/dns/clouddns/) and check for required config
|
||||
|
||||
3. Build `go-proxy` with `make build`
|
||||
|
||||
4. Set required config in `config.yml` `autocert` -> `options` section
|
||||
|
||||
```shell
|
||||
# From https://go-acme.github.io/lego/dns/clouddns/
|
||||
CLOUDDNS_CLIENT_ID=bLsdFAks23429841238feb177a572aX \
|
||||
CLOUDDNS_EMAIL=you@example.com \
|
||||
CLOUDDNS_PASSWORD=b9841238feb177a84330f \
|
||||
lego --email you@example.com --dns clouddns --domains my.example.org run
|
||||
```
|
||||
|
||||
Should turn into:
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
...
|
||||
options:
|
||||
client_id: bLsdFAks23429841238feb177a572aX
|
||||
email: you@example.com
|
||||
password: b9841238feb177a84330f
|
||||
```
|
||||
|
||||
5. Run with `GOPROXY_NO_SCHEMA_VALIDATION=1` and test if it works
|
||||
6. Commit and create pull request
|
||||
@@ -1,104 +0,0 @@
|
||||
# Benchmarks
|
||||
|
||||
Benchmarked with `wrk` and `traefik/whoami`'s `/bench` endpoint
|
||||
|
||||
## Remote benchmark
|
||||
|
||||
- Direct connection
|
||||
|
||||
```shell
|
||||
root@yusing-pc:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.100.3:8003/bench
|
||||
Running 10s test @ http://10.0.100.3:8003/bench
|
||||
10 threads and 200 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 94.75ms 199.92ms 1.68s 91.27%
|
||||
Req/Sec 4.24k 1.79k 18.79k 72.13%
|
||||
Latency Distribution
|
||||
50% 1.14ms
|
||||
75% 120.23ms
|
||||
90% 245.63ms
|
||||
99% 1.03s
|
||||
423444 requests in 10.10s, 50.88MB read
|
||||
Socket errors: connect 0, read 0, write 0, timeout 29
|
||||
Requests/sec: 41926.32
|
||||
Transfer/sec: 5.04MB
|
||||
```
|
||||
|
||||
- With reverse proxy
|
||||
|
||||
```shell
|
||||
root@yusing-pc:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.1.7/bench
|
||||
Running 10s test @ http://10.0.1.7/bench
|
||||
10 threads and 200 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 79.35ms 169.79ms 1.69s 92.55%
|
||||
Req/Sec 4.27k 1.90k 19.61k 75.81%
|
||||
Latency Distribution
|
||||
50% 1.12ms
|
||||
75% 105.66ms
|
||||
90% 200.22ms
|
||||
99% 814.59ms
|
||||
409836 requests in 10.10s, 49.25MB read
|
||||
Socket errors: connect 0, read 0, write 0, timeout 18
|
||||
Requests/sec: 40581.61
|
||||
Transfer/sec: 4.88MB
|
||||
```
|
||||
|
||||
## Local benchmark (client running wrk and `go-proxy` server are under same proxmox host but different LXCs)
|
||||
|
||||
- Direct connection
|
||||
|
||||
```shell
|
||||
root@http-benchmark-client:~# wrk -t 10 -c 200 -d 10s --latency http://10.0.100.1/bench
|
||||
Running 10s test @ http://10.0.100.1/bench
|
||||
10 threads and 200 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 434.08us 539.35us 8.76ms 85.28%
|
||||
Req/Sec 67.71k 6.31k 87.21k 71.20%
|
||||
Latency Distribution
|
||||
50% 153.00us
|
||||
75% 646.00us
|
||||
90% 1.18ms
|
||||
99% 2.38ms
|
||||
6739591 requests in 10.01s, 809.85MB read
|
||||
Requests/sec: 673608.15
|
||||
Transfer/sec: 80.94MB
|
||||
```
|
||||
|
||||
- With `go-proxy` reverse proxy
|
||||
|
||||
```shell
|
||||
root@http-benchmark-client:~# wrk -t 10 -c 200 -d 10s -H "Host: bench.6uo.me" --latency http://10.0.1.7/bench
|
||||
Running 10s test @ http://10.0.1.7/bench
|
||||
10 threads and 200 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 1.23ms 0.96ms 11.43ms 72.09%
|
||||
Req/Sec 17.48k 1.76k 21.48k 70.20%
|
||||
Latency Distribution
|
||||
50% 0.98ms
|
||||
75% 1.76ms
|
||||
90% 2.54ms
|
||||
99% 4.24ms
|
||||
1739079 requests in 10.01s, 208.97MB read
|
||||
Requests/sec: 173779.44
|
||||
Transfer/sec: 20.88MB
|
||||
```
|
||||
|
||||
- With `traefik-v3`
|
||||
|
||||
```shell
|
||||
root@traefik-benchmark:~# wrk -t10 -c200 -d10s -H "Host: benchmark.whoami" --latency http://127.0.0.1:8000/bench
|
||||
Running 10s test @ http://127.0.0.1:8000/bench
|
||||
10 threads and 200 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 2.81ms 10.36ms 180.26ms 98.57%
|
||||
Req/Sec 11.35k 1.74k 13.76k 85.54%
|
||||
Latency Distribution
|
||||
50% 1.59ms
|
||||
75% 2.27ms
|
||||
90% 3.17ms
|
||||
99% 37.91ms
|
||||
1125723 requests in 10.01s, 109.50MB read
|
||||
Requests/sec: 112499.59
|
||||
Transfer/sec: 10.94MB
|
||||
```
|
||||
@@ -1,81 +0,0 @@
|
||||
# Supported DNS Providers
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [Supported DNS Providers](#supported-dns-providers)
|
||||
- [Cloudflare](#cloudflare)
|
||||
- [CloudDNS](#clouddns)
|
||||
- [DuckDNS](#duckdns)
|
||||
- [OVHCloud](#ovhcloud)
|
||||
- [Implement other DNS providers](#implement-other-dns-providers)
|
||||
|
||||
## 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
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
provider: clouddns
|
||||
options:
|
||||
client_id:
|
||||
email:
|
||||
password:
|
||||
```
|
||||
|
||||
## DuckDNS
|
||||
|
||||
```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`
|
||||
|
||||
## Implement other DNS providers
|
||||
|
||||
See [add_dns_provider.md](docs/add_dns_provider.md)
|
||||
296
docs/docker.md
296
docs/docker.md
@@ -1,296 +0,0 @@
|
||||
# Docker compose guide
|
||||
|
||||
## Table of content
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [Docker compose guide](#docker-compose-guide)
|
||||
- [Table of content](#table-of-content)
|
||||
- [Additional setup](#additional-setup)
|
||||
- [Labels](#labels)
|
||||
- [Syntax](#syntax)
|
||||
- [Fields](#fields)
|
||||
- [Key-value mapping example](#key-value-mapping-example)
|
||||
- [List example](#list-example)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Docker compose examples](#docker-compose-examples)
|
||||
- [Services URLs for above examples](#services-urls-for-above-examples)
|
||||
|
||||
## Additional setup
|
||||
|
||||
1. Enable HTTPs _(optional)_
|
||||
|
||||
Mount a folder to store obtained certs or to load existing cert
|
||||
|
||||
```yaml
|
||||
services:
|
||||
go-proxy:
|
||||
...
|
||||
volumes:
|
||||
- ./certs:/app/certs
|
||||
```
|
||||
|
||||
To use **autocert**, complete that section in `config.yml`, e.g.
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
email: john.doe@x.y.z # ACME Email
|
||||
domains: # a list of domains for cert registration
|
||||
- y.z
|
||||
- *.y.z
|
||||
provider: cloudflare
|
||||
options:
|
||||
auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
```
|
||||
|
||||
To use **existing certificate**, set path for cert and key in `config.yml`, e.g.
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
provider: local
|
||||
cert_path: /app/certs/cert.crt
|
||||
key_path: /app/certs/priv.key
|
||||
```
|
||||
|
||||
2. Modify `compose.yml` to fit your needs
|
||||
|
||||
3. Run `docker compose up -d` to start the container
|
||||
|
||||
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 | | `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` | 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)
|
||||
|
||||
#### Key-value mapping example
|
||||
|
||||
Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx:
|
||||
...
|
||||
labels:
|
||||
proxy.nginx.middlewares.modify_request.set_headers: | # remember to add the '|'
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3, value4
|
||||
```
|
||||
|
||||
File Provider
|
||||
|
||||
```yaml
|
||||
service_a:
|
||||
host: service_a.internal
|
||||
middlewares:
|
||||
modify_request:
|
||||
set_headers:
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
#### List example
|
||||
|
||||
Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx:
|
||||
...
|
||||
labels:
|
||||
proxy.nginx.path_patterns: | # remember to add the '|'
|
||||
- GET /
|
||||
- POST /auth
|
||||
proxy.nginx.middlewares.modify_request.hide_headers: | # remember to add the '|'
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
```
|
||||
|
||||
Include file
|
||||
|
||||
```yaml
|
||||
service_a:
|
||||
host: service_a.internal
|
||||
path_patterns:
|
||||
- GET /
|
||||
- POST /auth
|
||||
middlewares:
|
||||
modify_request:
|
||||
hide_headers:
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Container not showing up in proxies list
|
||||
|
||||
Please check that either `ports` or label `proxy.<alias>.port` is declared, e.g.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx-1: # Option 1
|
||||
...
|
||||
ports:
|
||||
- 80
|
||||
nginx-2: # Option 2
|
||||
...
|
||||
container_name: nginx-2
|
||||
network_mode: host
|
||||
labels:
|
||||
proxy.nginx-2.port: 80
|
||||
```
|
||||
|
||||
- Firewall issues
|
||||
|
||||
If you are using `ufw` with vpn that drop all inbound traffic except vpn, run below:
|
||||
|
||||
`sudo ufw allow from 172.16.0.0/16 to 100.64.0.0/10`
|
||||
|
||||
Explaination:
|
||||
|
||||
Docker network is usually `172.16.0.0/16`
|
||||
|
||||
Tailscale is used as an example, `100.64.0.0/10` will be the CIDR
|
||||
|
||||
You can also list CIDRs of all docker bridge networks by:
|
||||
|
||||
`docker network inspect $(docker network ls | awk '$3 == "bridge" { print $1}') | jq -r '.[] | .Name + " " + .IPAM.Config[0].Subnet' -`
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Docker compose examples
|
||||
|
||||
More examples in [here](examples/)
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
adg-work:
|
||||
adg-conf:
|
||||
mc-data:
|
||||
palworld:
|
||||
nginx:
|
||||
services:
|
||||
adg:
|
||||
image: adguard/adguardhome
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- proxy.aliases=adg,adg-dns,adg-setup
|
||||
- proxy.#1.port=80
|
||||
- proxy.#2.scheme=udp
|
||||
- proxy.#2.port=20000:dns
|
||||
- proxy.#3.port=3000
|
||||
volumes:
|
||||
- adg-work:/opt/adguardhome/work
|
||||
- adg-conf:/opt/adguardhome/conf
|
||||
ports:
|
||||
- 80
|
||||
- 3000
|
||||
- 53/udp
|
||||
mc:
|
||||
image: itzg/minecraft-server
|
||||
tty: true
|
||||
stdin_open: true
|
||||
container_name: mc
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 25565
|
||||
labels:
|
||||
- proxy.mc.port=20001:25565
|
||||
environment:
|
||||
- EULA=TRUE
|
||||
volumes:
|
||||
- mc-data:/data
|
||||
palworld:
|
||||
image: thijsvanloef/palworld-server-docker:latest
|
||||
restart: unless-stopped
|
||||
container_name: pal
|
||||
stop_grace_period: 30s
|
||||
ports:
|
||||
- 8211/udp
|
||||
- 27015/udp
|
||||
labels:
|
||||
- proxy.aliases=pal1,pal2
|
||||
- proxy.*.scheme=udp
|
||||
- proxy.#1.port=20002:8211
|
||||
- proxy.#2.port=20003:27015
|
||||
environment: ...
|
||||
volumes:
|
||||
- palworld:/palworld
|
||||
nginx:
|
||||
image: nginx
|
||||
container_name: nginx
|
||||
volumes:
|
||||
- nginx:/usr/share/nginx/html
|
||||
ports:
|
||||
- 80
|
||||
labels:
|
||||
proxy.idle_timeout: 1m
|
||||
go-proxy:
|
||||
image: ghcr.io/yusing/go-proxy:latest
|
||||
container_name: go-proxy
|
||||
restart: always
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
go-proxy-frontend:
|
||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
||||
container_name: go-proxy-frontend
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
labels:
|
||||
- proxy.aliases=gp
|
||||
- proxy.gp.port=3000
|
||||
depends_on:
|
||||
- go-proxy
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Services URLs for above examples
|
||||
|
||||
- `gp.yourdomain.com`: go-proxy web panel
|
||||
- `adg-setup.yourdomain.com`: adguard setup (first time setup)
|
||||
- `adg.yourdomain.com`: adguard dashboard
|
||||
- `nginx.yourdomain.com`: nginx
|
||||
- `yourdomain.com:2000`: adguard dns (udp)
|
||||
- `yourdomain.com:20001`: minecraft server
|
||||
- `yourdomain.com:20002`: palworld server
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
@@ -1,40 +0,0 @@
|
||||
## Docker Socket Proxy
|
||||
|
||||
For docker client on other machine, set this up, then add `name: tcp://<machine_ip>:2375` to `config.yml` under `docker` section
|
||||
|
||||
```yml
|
||||
# compose.yml on remote machine (e.g. server1)
|
||||
docker-proxy:
|
||||
container_name: docker-proxy
|
||||
image: tecnativa/docker-socket-proxy
|
||||
privileged: true
|
||||
environment:
|
||||
- ALLOW_START=1
|
||||
- ALLOW_STOP=1
|
||||
- ALLOW_RESTARTS=1
|
||||
- CONTAINERS=1
|
||||
- EVENTS=1
|
||||
- PING=1
|
||||
- POST=1
|
||||
- VERSION=1
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
restart: always
|
||||
ports:
|
||||
- 2375:2375
|
||||
# or more secure
|
||||
- <machine_private_ip>:2375:2375
|
||||
```
|
||||
|
||||
```yml
|
||||
# config.yml on go-proxy machine
|
||||
autocert:
|
||||
... # your config
|
||||
|
||||
providers:
|
||||
include:
|
||||
...
|
||||
docker:
|
||||
...
|
||||
server1: tcp://<machine_ip>:2375
|
||||
```
|
||||
@@ -1,333 +0,0 @@
|
||||
# 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)
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
limits:
|
||||
memory: 256M
|
||||
env_file: .env
|
||||
image: docker.i.sh/danielszabo99/microbin:latest
|
||||
image: danielszabo99/microbin:latest
|
||||
ports:
|
||||
- 8080
|
||||
restart: unless-stopped
|
||||
|
||||
17
go.mod
17
go.mod
@@ -1,16 +1,18 @@
|
||||
module github.com/yusing/go-proxy
|
||||
|
||||
go 1.23.1
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
github.com/coder/websocket v1.8.12
|
||||
github.com/docker/cli v27.3.1+incompatible
|
||||
github.com/docker/docker v27.3.1+incompatible
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/go-acme/lego/v4 v4.18.0
|
||||
github.com/go-acme/lego/v4 v4.19.2
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
||||
github.com/santhosh-tekuri/jsonschema v1.2.4
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
golang.org/x/net v0.29.0
|
||||
golang.org/x/net v0.30.0
|
||||
golang.org/x/text v0.19.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -45,14 +47,13 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.30.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/crypto v0.28.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/tools v0.25.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
|
||||
38
go.sum
38
go.sum
@@ -6,12 +6,15 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
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/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
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=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
|
||||
@@ -26,8 +29,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-acme/lego/v4 v4.18.0 h1:2hH8KcdRBSb+p5o9VZIm61GAOXYALgILUCSs1Q+OYsk=
|
||||
github.com/go-acme/lego/v4 v4.18.0/go.mod h1:Blkg3izvXpl3zxk7WKngIuwR2I/hvYVP3vRnvgBp7m8=
|
||||
github.com/go-acme/lego/v4 v4.19.2 h1:Y8hrmMvWETdqzzkRly7m98xtPJJivWFsgWi8fcvZo+Y=
|
||||
github.com/go-acme/lego/v4 v4.19.2/go.mod h1:wtDe3dDkmV4/oI2nydpNXSJpvV10J9RCyZ6MbYxNtlQ=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -75,8 +78,9 @@ github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC
|
||||
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
@@ -111,8 +115,8 @@ go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
@@ -121,8 +125,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -134,25 +138,25 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
|
||||
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
|
||||
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=
|
||||
|
||||
@@ -24,26 +24,31 @@ func (mux ServeMux) HandleFunc(method, endpoint string, handler http.HandlerFunc
|
||||
func NewHandler(cfg *config.Config) http.Handler {
|
||||
mux := NewServeMux()
|
||||
mux.HandleFunc("GET", "/v1", v1.Index)
|
||||
mux.HandleFunc("GET", "/v1/version", v1.GetVersion)
|
||||
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/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/stats/ws", wrap(cfg, v1.StatsWS))
|
||||
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 {
|
||||
if common.IsDebug {
|
||||
return f
|
||||
}
|
||||
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.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte("invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ func CheckHealth(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
U.HandleErr(w, r, U.ErrNotFound("target", target), http.StatusNotFound)
|
||||
return
|
||||
case route.Type() == R.RouteTypeReverseProxy:
|
||||
ok = U.IsSiteHealthy(route.URL().String())
|
||||
ok = IsSiteHealthy(route.URL().String())
|
||||
case route.Type() == R.RouteTypeStream:
|
||||
entry := route.Entry()
|
||||
ok = U.IsStreamHealthy(
|
||||
ok = IsStreamHealthy(
|
||||
strings.Split(entry.Scheme, ":")[1], // target scheme
|
||||
fmt.Sprintf("%s:%v", entry.Host, strings.Split(entry.Port, ":")[1]),
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
@@ -41,7 +42,7 @@ func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||
var validateErr E.NestedError
|
||||
if filename == common.ConfigFileName {
|
||||
validateErr = config.Validate(content)
|
||||
} else {
|
||||
} else if !strings.HasPrefix(filename, path.Base(common.MiddlewareComposeBasePath)) {
|
||||
validateErr = provider.Validate(content)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
package utils
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"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)
|
||||
resp, err := U.Head(url)
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
if err != nil && resp != nil && resp.StatusCode == http.StatusMethodNotAllowed {
|
||||
_, err = httpClient.Get(url)
|
||||
_, err = U.Get(url)
|
||||
}
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
@@ -1,26 +1,44 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
U "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/net/http/middleware"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
ListRoutes = "routes"
|
||||
ListConfigFiles = "config_files"
|
||||
ListMiddlewares = "middlewares"
|
||||
ListMiddlewareTrace = "middleware_trace"
|
||||
ListMatchDomains = "match_domains"
|
||||
ListHomepageConfig = "homepage_config"
|
||||
)
|
||||
|
||||
func List(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
what := r.PathValue("what")
|
||||
if what == "" {
|
||||
what = "routes"
|
||||
what = ListRoutes
|
||||
}
|
||||
|
||||
switch what {
|
||||
case "routes":
|
||||
case ListRoutes:
|
||||
listRoutes(cfg, w, r)
|
||||
case "config_files":
|
||||
case ListConfigFiles:
|
||||
listConfigFiles(w, r)
|
||||
case ListMiddlewares:
|
||||
listMiddlewares(w, r)
|
||||
case ListMiddlewareTrace:
|
||||
listMiddlewareTrace(w, r)
|
||||
case ListMatchDomains:
|
||||
listMatchDomains(cfg, w, r)
|
||||
case ListHomepageConfig:
|
||||
listHomepageConfig(cfg, w, r)
|
||||
default:
|
||||
U.HandleErr(w, r, U.ErrInvalidKey("what"), http.StatusBadRequest)
|
||||
}
|
||||
@@ -37,25 +55,33 @@ func listRoutes(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := U.RespondJson(w, routes); err != nil {
|
||||
U.HandleErr(w, r, err)
|
||||
}
|
||||
U.HandleErr(w, r, U.RespondJson(w, routes))
|
||||
}
|
||||
|
||||
func listConfigFiles(w http.ResponseWriter, r *http.Request) {
|
||||
files, err := os.ReadDir(common.ConfigBasePath)
|
||||
files, err := utils.ListFiles(common.ConfigBasePath, 1)
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, err)
|
||||
return
|
||||
}
|
||||
filenames := make([]string, len(files))
|
||||
for i, f := range files {
|
||||
filenames[i] = f.Name()
|
||||
for i := range files {
|
||||
files[i] = strings.TrimPrefix(files[i], common.ConfigBasePath+"/")
|
||||
}
|
||||
resp, err := json.Marshal(filenames)
|
||||
if err != nil {
|
||||
U.HandleErr(w, r, err)
|
||||
return
|
||||
}
|
||||
w.Write(resp)
|
||||
U.HandleErr(w, r, U.RespondJson(w, files))
|
||||
}
|
||||
|
||||
func listMiddlewareTrace(w http.ResponseWriter, r *http.Request) {
|
||||
U.HandleErr(w, r, U.RespondJson(w, middleware.GetAllTrace()))
|
||||
}
|
||||
|
||||
func listMiddlewares(w http.ResponseWriter, r *http.Request) {
|
||||
U.HandleErr(w, r, U.RespondJson(w, middleware.All()))
|
||||
}
|
||||
|
||||
func listMatchDomains(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
U.HandleErr(w, r, U.RespondJson(w, cfg.Value().MatchDomains))
|
||||
}
|
||||
|
||||
func listHomepageConfig(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
U.HandleErr(w, r, U.RespondJson(w, cfg.HomepageConfig()))
|
||||
}
|
||||
|
||||
69
internal/api/v1/query/query.go
Normal file
69
internal/api/v1/query/query.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||
)
|
||||
|
||||
func ReloadServer() E.NestedError {
|
||||
resp, err := U.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
|
||||
}
|
||||
|
||||
func ListRoutes() (map[string]map[string]any, E.NestedError) {
|
||||
resp, err := U.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, v1.ListRoutes))
|
||||
if err != nil {
|
||||
return nil, E.From(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, E.Failure("list routes").Extraf("status code: %v", resp.StatusCode)
|
||||
}
|
||||
var routes map[string]map[string]any
|
||||
err = json.NewDecoder(resp.Body).Decode(&routes)
|
||||
if err != nil {
|
||||
return nil, E.From(err)
|
||||
}
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
func ListMiddlewareTraces() (middleware.Traces, E.NestedError) {
|
||||
resp, err := U.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, v1.ListMiddlewareTrace))
|
||||
if err != nil {
|
||||
return nil, E.From(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, E.Failure("list middleware trace").Extraf("status code: %v", resp.StatusCode)
|
||||
}
|
||||
var traces middleware.Traces
|
||||
err = json.NewDecoder(resp.Body).Decode(&traces)
|
||||
if err != nil {
|
||||
return nil, E.From(err)
|
||||
}
|
||||
return traces, nil
|
||||
}
|
||||
@@ -1,20 +1,67 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
U "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/server"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
)
|
||||
|
||||
func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
stats := map[string]any{
|
||||
U.HandleErr(w, r, U.RespondJson(w, getStats(cfg)))
|
||||
}
|
||||
|
||||
func StatsWS(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
|
||||
originPats := make([]string, len(cfg.Value().MatchDomains)+len(localAddresses))
|
||||
|
||||
if len(originPats) == 0 {
|
||||
U.Logger.Warnf("no match domains configured, accepting websocket request from all origins")
|
||||
originPats = []string{"*"}
|
||||
} else {
|
||||
for i, domain := range cfg.Value().MatchDomains {
|
||||
originPats[i] = "*." + domain
|
||||
}
|
||||
originPats = append(originPats, localAddresses...)
|
||||
}
|
||||
if common.IsDebug {
|
||||
originPats = []string{"*"}
|
||||
}
|
||||
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||
OriginPatterns: originPats,
|
||||
})
|
||||
if err != nil {
|
||||
U.Logger.Errorf("/stats/ws failed to upgrade websocket: %s", err)
|
||||
return
|
||||
}
|
||||
defer conn.CloseNow()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
stats := getStats(cfg)
|
||||
if err := wsjson.Write(ctx, conn, stats); err != nil {
|
||||
U.Logger.Errorf("/stats/ws failed to write JSON: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getStats(cfg *config.Config) map[string]any {
|
||||
return map[string]any{
|
||||
"proxies": cfg.Statistics(),
|
||||
"uptime": utils.FormatDuration(server.GetProxyServer().Uptime()),
|
||||
}
|
||||
if err := U.RespondJson(w, stats); err != nil {
|
||||
U.HandleErr(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import (
|
||||
var Logger = logrus.WithField("module", "api")
|
||||
|
||||
func HandleErr(w http.ResponseWriter, r *http.Request, origErr error, code ...int) {
|
||||
if origErr == nil {
|
||||
return
|
||||
}
|
||||
err := E.From(origErr).Subjectf("%s %s", r.Method, r.URL)
|
||||
Logger.Error(err)
|
||||
if len(code) > 0 {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{
|
||||
var HTTPClient = &http.Client{
|
||||
Timeout: common.ConnectionTimeout,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
@@ -21,3 +21,7 @@ var httpClient = &http.Client{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
var Get = HTTPClient.Get
|
||||
var Post = HTTPClient.Post
|
||||
var Head = HTTPClient.Head
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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
|
||||
}
|
||||
11
internal/api/v1/version.go
Normal file
11
internal/api/v1/version.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg"
|
||||
)
|
||||
|
||||
func GetVersion(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(pkg.GetVersion()))
|
||||
}
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
M "github.com/yusing/go-proxy/internal/models"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
type Config M.AutoCertConfig
|
||||
type Config types.AutoCertConfig
|
||||
|
||||
func NewConfig(cfg *M.AutoCertConfig) *Config {
|
||||
func NewConfig(cfg *types.AutoCertConfig) *Config {
|
||||
if cfg.CertPath == "" {
|
||||
cfg.CertPath = CertFileDefault
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import (
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
M "github.com/yusing/go-proxy/internal/models"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
@@ -29,7 +30,7 @@ type Provider struct {
|
||||
certExpiries CertExpiries
|
||||
}
|
||||
|
||||
type ProviderGenerator func(M.AutocertProviderOpt) (challenge.Provider, E.NestedError)
|
||||
type ProviderGenerator func(types.AutocertProviderOpt) (challenge.Provider, E.NestedError)
|
||||
type CertExpiries map[string]time.Time
|
||||
|
||||
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
@@ -280,7 +281,7 @@ func providerGenerator[CT any, PT challenge.Provider](
|
||||
defaultCfg func() *CT,
|
||||
newProvider func(*CT) (PT, error),
|
||||
) ProviderGenerator {
|
||||
return func(opt M.AutocertProviderOpt) (challenge.Provider, E.NestedError) {
|
||||
return func(opt types.AutocertProviderOpt) (challenge.Provider, E.NestedError) {
|
||||
cfg := defaultCfg()
|
||||
err := U.Deserialize(opt, cfg)
|
||||
if err.HasError() {
|
||||
|
||||
@@ -45,6 +45,6 @@ oauth2_config:
|
||||
testYaml = testYaml[1:] // remove first \n
|
||||
opt := make(map[string]any)
|
||||
ExpectNoError(t, yaml.Unmarshal([]byte(testYaml), opt))
|
||||
ExpectTrue(t, U.Deserialize(opt, cfg).NoError())
|
||||
ExpectNoError(t, U.Deserialize(opt, cfg).Error())
|
||||
ExpectDeepEqual(t, cfg, cfgExpected)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package common
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
@@ -17,9 +17,11 @@ const (
|
||||
CommandValidate = "validate"
|
||||
CommandListConfigs = "ls-config"
|
||||
CommandListRoutes = "ls-routes"
|
||||
CommandListIcons = "ls-icons"
|
||||
CommandReload = "reload"
|
||||
CommandDebugListEntries = "debug-ls-entries"
|
||||
CommandDebugListProviders = "debug-ls-providers"
|
||||
CommandDebugListMTrace = "debug-ls-mtrace"
|
||||
)
|
||||
|
||||
var ValidCommands = []string{
|
||||
@@ -28,26 +30,28 @@ var ValidCommands = []string{
|
||||
CommandValidate,
|
||||
CommandListConfigs,
|
||||
CommandListRoutes,
|
||||
CommandListIcons,
|
||||
CommandReload,
|
||||
CommandDebugListEntries,
|
||||
CommandDebugListProviders,
|
||||
CommandDebugListMTrace,
|
||||
}
|
||||
|
||||
func GetArgs() Args {
|
||||
var args Args
|
||||
flag.Parse()
|
||||
args.Command = flag.Arg(0)
|
||||
if err := validateArg(args.Command); err.HasError() {
|
||||
if err := validateArg(args.Command); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func validateArg(arg string) E.NestedError {
|
||||
func validateArg(arg string) error {
|
||||
for _, v := range ValidCommands {
|
||||
if arg == v {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return E.Invalid("argument", arg)
|
||||
return fmt.Errorf("invalid command: %s", arg)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ const (
|
||||
ConfigFileName = "config.yml"
|
||||
ConfigExampleFileName = "config.example.yml"
|
||||
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
||||
|
||||
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,6 +41,7 @@ var (
|
||||
ConfigBasePath,
|
||||
SchemaBasePath,
|
||||
ErrorPagesBasePath,
|
||||
MiddlewareComposeBasePath,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -2,16 +2,19 @@ package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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)
|
||||
NoSchemaValidation = GetEnvBool("GOPROXY_NO_SCHEMA_VALIDATION", true)
|
||||
IsTest = GetEnvBool("GOPROXY_TEST", false) || strings.HasSuffix(os.Args[0], ".test")
|
||||
IsDebug = GetEnvBool("GOPROXY_DEBUG", IsTest)
|
||||
|
||||
ProxyHTTPAddr,
|
||||
ProxyHTTPHost,
|
||||
@@ -34,7 +37,11 @@ func GetEnvBool(key string, defaultValue bool) bool {
|
||||
if !ok || value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return U.ParseBool(value)
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid boolean value: %s", value)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func GetEnv(key, defaultValue string) string {
|
||||
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
"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"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
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"
|
||||
@@ -19,7 +20,7 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
value *M.Config
|
||||
value *types.Config
|
||||
proxyProviders F.Map[string, *PR.Provider]
|
||||
autocertProvider *autocert.Provider
|
||||
|
||||
@@ -42,7 +43,7 @@ func Load() E.NestedError {
|
||||
return nil
|
||||
}
|
||||
instance = &Config{
|
||||
value: M.DefaultConfig(),
|
||||
value: types.DefaultConfig(),
|
||||
proxyProviders: F.NewMapOf[string, *PR.Provider](),
|
||||
l: logrus.WithField("module", "config"),
|
||||
watcher: W.NewConfigFileWatcher(common.ConfigFileName),
|
||||
@@ -62,7 +63,7 @@ func MatchDomains() []string {
|
||||
return instance.value.MatchDomains
|
||||
}
|
||||
|
||||
func (cfg *Config) Value() M.Config {
|
||||
func (cfg *Config) Value() types.Config {
|
||||
if cfg == nil {
|
||||
logrus.Panic("config has not been loaded, please check if there is any errors")
|
||||
}
|
||||
@@ -158,7 +159,7 @@ func (cfg *Config) load() (res E.NestedError) {
|
||||
}
|
||||
}
|
||||
|
||||
model := M.DefaultConfig()
|
||||
model := types.DefaultConfig()
|
||||
if err := E.From(yaml.Unmarshal(data, model)); err.HasError() {
|
||||
b.Add(E.FailWith("parse config", err))
|
||||
logrus.Fatal(b.Build())
|
||||
@@ -173,7 +174,7 @@ func (cfg *Config) load() (res E.NestedError) {
|
||||
return
|
||||
}
|
||||
|
||||
func (cfg *Config) initAutoCert(autocertCfg *M.AutoCertConfig) (err E.NestedError) {
|
||||
func (cfg *Config) initAutoCert(autocertCfg *types.AutoCertConfig) (err E.NestedError) {
|
||||
if cfg.autocertProvider != nil {
|
||||
return
|
||||
}
|
||||
@@ -188,7 +189,7 @@ func (cfg *Config) initAutoCert(autocertCfg *M.AutoCertConfig) (err E.NestedErro
|
||||
return
|
||||
}
|
||||
|
||||
func (cfg *Config) loadProviders(providers *M.ProxyProviders) (res E.NestedError) {
|
||||
func (cfg *Config) loadProviders(providers *types.ProxyProviders) (res E.NestedError) {
|
||||
cfg.l.Debug("loading providers")
|
||||
defer cfg.l.Debug("loaded providers")
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
M "github.com/yusing/go-proxy/internal/models"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
H "github.com/yusing/go-proxy/internal/homepage"
|
||||
PR "github.com/yusing/go-proxy/internal/proxy/provider"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
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 {
|
||||
entries := make(map[string]*M.RawEntry)
|
||||
func (cfg *Config) DumpEntries() map[string]*types.RawEntry {
|
||||
entries := make(map[string]*types.RawEntry)
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
entries[alias] = r.Entry()
|
||||
})
|
||||
@@ -24,9 +29,80 @@ func (cfg *Config) DumpProviders() map[string]*PR.Provider {
|
||||
return entries
|
||||
}
|
||||
|
||||
func (cfg *Config) HomepageConfig() H.HomePageConfig {
|
||||
var proto, port string
|
||||
domains := cfg.value.MatchDomains
|
||||
cert, _ := cfg.autocertProvider.GetCert(nil)
|
||||
if cert != nil {
|
||||
proto = "https"
|
||||
port = common.ProxyHTTPSPort
|
||||
} else {
|
||||
proto = "http"
|
||||
port = common.ProxyHTTPPort
|
||||
}
|
||||
|
||||
hpCfg := H.NewHomePageConfig()
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
if !r.Started() {
|
||||
return
|
||||
}
|
||||
|
||||
entry := r.Entry()
|
||||
if entry.Homepage == nil {
|
||||
entry.Homepage = &H.HomePageItem{
|
||||
Show: r.Entry().IsExplicit || !p.IsExplicitOnly(),
|
||||
}
|
||||
}
|
||||
|
||||
item := entry.Homepage
|
||||
|
||||
if !item.Show && !item.IsEmpty() {
|
||||
item.Show = true
|
||||
}
|
||||
|
||||
if !item.Show || r.Type() != R.RouteTypeReverseProxy {
|
||||
return
|
||||
}
|
||||
|
||||
if item.Name == "" {
|
||||
item.Name = U.Title(
|
||||
strings.ReplaceAll(
|
||||
strings.ReplaceAll(alias, "-", " "),
|
||||
"_", " ",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if p.GetType() == PR.ProviderTypeDocker {
|
||||
if item.Category == "" {
|
||||
item.Category = "Docker"
|
||||
}
|
||||
item.SourceType = string(PR.ProviderTypeDocker)
|
||||
} else if p.GetType() == PR.ProviderTypeFile {
|
||||
if item.Category == "" {
|
||||
item.Category = "Others"
|
||||
}
|
||||
item.SourceType = string(PR.ProviderTypeFile)
|
||||
}
|
||||
|
||||
if item.URL == "" {
|
||||
if len(domains) > 0 {
|
||||
item.URL = fmt.Sprintf("%s://%s.%s:%s", proto, strings.ToLower(alias), domains[0], port)
|
||||
}
|
||||
}
|
||||
item.AltURL = r.URL().String()
|
||||
|
||||
hpCfg.Add(item)
|
||||
})
|
||||
return hpCfg
|
||||
}
|
||||
|
||||
func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject {
|
||||
routes := make(map[string]U.SerializedObject)
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
if !r.Started() {
|
||||
return
|
||||
}
|
||||
obj, err := U.Serialize(r)
|
||||
if err.HasError() {
|
||||
cfg.l.Error(err)
|
||||
@@ -34,6 +110,8 @@ func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject {
|
||||
}
|
||||
obj["provider"] = p.GetName()
|
||||
obj["type"] = string(r.Type())
|
||||
obj["started"] = r.Started()
|
||||
obj["raw"] = r.Entry()
|
||||
routes[alias] = obj
|
||||
})
|
||||
return routes
|
||||
@@ -42,27 +120,17 @@ func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject {
|
||||
func (cfg *Config) Statistics() map[string]any {
|
||||
nTotalStreams := 0
|
||||
nTotalRPs := 0
|
||||
providerStats := make(map[string]any)
|
||||
providerStats := make(map[string]PR.ProviderStats)
|
||||
|
||||
cfg.forEachRoute(func(alias string, r R.Route, p *PR.Provider) {
|
||||
s, ok := providerStats[p.GetName()]
|
||||
if !ok {
|
||||
s = make(map[string]int)
|
||||
}
|
||||
|
||||
stats := s.(map[string]int)
|
||||
switch r.Type() {
|
||||
case R.RouteTypeStream:
|
||||
stats["num_streams"]++
|
||||
nTotalStreams++
|
||||
case R.RouteTypeReverseProxy:
|
||||
stats["num_reverse_proxies"]++
|
||||
nTotalRPs++
|
||||
default:
|
||||
panic("bug: should not reach here")
|
||||
}
|
||||
cfg.proxyProviders.RangeAll(func(name string, p *PR.Provider) {
|
||||
providerStats[name] = p.Statistics()
|
||||
})
|
||||
|
||||
for _, stats := range providerStats {
|
||||
nTotalRPs += stats.NumRPs
|
||||
nTotalStreams += stats.NumStreams
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"num_total_streams": nTotalStreams,
|
||||
"num_total_reverse_proxies": nTotalRPs,
|
||||
|
||||
@@ -20,6 +20,7 @@ func FromDocker(c *types.Container, dockerHost string) (res Container) {
|
||||
res.ProxyProperties = &ProxyProperties{
|
||||
DockerHost: dockerHost,
|
||||
ContainerName: res.getName(),
|
||||
ContainerID: c.ID,
|
||||
ImageName: res.getImageName(),
|
||||
PublicPortMapping: res.getPublicPortMapping(),
|
||||
PrivatePortMapping: res.getPrivatePortMapping(),
|
||||
@@ -27,6 +28,7 @@ func FromDocker(c *types.Container, dockerHost string) (res Container) {
|
||||
Aliases: res.getAliases(),
|
||||
IsExcluded: U.ParseBool(res.getDeleteLabel(LabelExclude)),
|
||||
IsExplicit: isExplicit,
|
||||
IsDatabase: res.isDatabase(),
|
||||
IdleTimeout: res.getDeleteLabel(LabelIdleTimeout),
|
||||
WakeTimeout: res.getDeleteLabel(LabelWakeTimeout),
|
||||
StopMethod: res.getDeleteLabel(LabelStopMethod),
|
||||
@@ -107,3 +109,35 @@ func (c Container) getPrivatePortMapping() PortMapping {
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
var databaseMPs = map[string]struct{}{
|
||||
"/var/lib/postgresql/data": {},
|
||||
"/var/lib/mysql": {},
|
||||
"/var/lib/mongodb": {},
|
||||
"/var/lib/mariadb": {},
|
||||
"/var/lib/memcached": {},
|
||||
"/var/lib/rabbitmq": {},
|
||||
}
|
||||
|
||||
var databasePrivPorts = map[uint16]struct{}{
|
||||
5432: {}, // postgres
|
||||
3306: {}, // mysql, mariadb
|
||||
6379: {}, // redis
|
||||
11211: {}, // memcached
|
||||
27017: {}, // mongodb
|
||||
}
|
||||
|
||||
func (c Container) isDatabase() bool {
|
||||
for _, m := range c.Container.Mounts {
|
||||
if _, ok := databaseMPs[m.Destination]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range c.Ports {
|
||||
if _, ok := databasePrivPorts[v.PrivatePort]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package docker
|
||||
|
||||
type (
|
||||
HomePageConfig struct{ m map[string]HomePageCategory }
|
||||
HomePageCategory []HomePageItem
|
||||
|
||||
HomePageItem struct {
|
||||
Name string
|
||||
Icon string
|
||||
Category string
|
||||
Description string
|
||||
WidgetConfig map[string]any
|
||||
}
|
||||
)
|
||||
|
||||
func NewHomePageConfig() *HomePageConfig {
|
||||
return &HomePageConfig{m: make(map[string]HomePageCategory)}
|
||||
}
|
||||
|
||||
func NewHomePageItem() *HomePageItem {
|
||||
return &HomePageItem{}
|
||||
}
|
||||
|
||||
func (c *HomePageConfig) Clear() {
|
||||
c.m = make(map[string]HomePageCategory)
|
||||
}
|
||||
|
||||
func (c *HomePageConfig) Add(item HomePageItem) {
|
||||
c.m[item.Category] = HomePageCategory{item}
|
||||
}
|
||||
|
||||
const NSHomePage = "homepage"
|
||||
@@ -66,22 +66,23 @@
|
||||
<body>
|
||||
<script>
|
||||
window.onload = async function () {
|
||||
let result = await fetch(window.location.href, {
|
||||
let resp = await fetch(window.location.href, {
|
||||
headers: {
|
||||
{{ range $key, $value := .RequestHeaders }}
|
||||
'{{ $key }}' : {{ $value }}
|
||||
{{ end }}
|
||||
"{{.CheckRedirectHeader}}": "1",
|
||||
},
|
||||
}).then((resp) => resp.text())
|
||||
.catch((err) => {
|
||||
document.getElementById("message").innerText = err;
|
||||
});
|
||||
if (result) {
|
||||
document.documentElement.innerHTML = result
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.href = resp.url;
|
||||
} else {
|
||||
document.getElementById("message").innerText =
|
||||
await resp.text();
|
||||
document
|
||||
.getElementById("spinner")
|
||||
.classList.replace("spinner", "error");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<div class="{{.SpinnerClass}}"></div>
|
||||
<div class="message">{{.Message}}</div>
|
||||
<div id="spinner" class="spinner"></div>
|
||||
<div id="message" class="message">{{.Message}}</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,84 +4,35 @@ import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type templateData struct {
|
||||
Title string
|
||||
Message string
|
||||
RequestHeaders http.Header
|
||||
SpinnerClass string
|
||||
CheckRedirectHeader string
|
||||
Title string
|
||||
Message string
|
||||
}
|
||||
|
||||
//go:embed html/loading_page.html
|
||||
var loadingPage []byte
|
||||
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
|
||||
|
||||
const (
|
||||
htmlContentType = "text/html; charset=utf-8"
|
||||
const headerCheckRedirect = "X-GoProxy-Check-Redirect"
|
||||
|
||||
errPrefix = "\u1000"
|
||||
|
||||
headerGoProxyTargetURL = "X-GoProxy-Target"
|
||||
headerContentType = "Content-Type"
|
||||
|
||||
spinnerClassSpinner = "spinner"
|
||||
spinnerClassErrorSign = "error"
|
||||
)
|
||||
|
||||
func (w *watcher) makeSuccResp(redirectURL string, resp *http.Response) (*http.Response, error) {
|
||||
h := make(http.Header)
|
||||
h.Set("Location", redirectURL)
|
||||
h.Set("Content-Length", "0")
|
||||
h.Set(headerContentType, htmlContentType)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusTemporaryRedirect,
|
||||
Header: h,
|
||||
Body: http.NoBody,
|
||||
TLS: resp.TLS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *watcher) makeErrResp(errFmt string, args ...any) (*http.Response, error) {
|
||||
return w.makeResp(errPrefix+errFmt, args...)
|
||||
}
|
||||
|
||||
func (w *watcher) makeResp(format string, args ...any) (*http.Response, error) {
|
||||
func (w *watcher) makeRespBody(format string, args ...any) []byte {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
|
||||
data := new(templateData)
|
||||
data.CheckRedirectHeader = headerCheckRedirect
|
||||
data.Title = w.ContainerName
|
||||
data.Message = strings.ReplaceAll(msg, "\n", "<br>")
|
||||
data.Message = strings.ReplaceAll(data.Message, " ", " ")
|
||||
data.RequestHeaders = make(http.Header)
|
||||
data.RequestHeaders.Add(headerGoProxyTargetURL, "window.location.href")
|
||||
if strings.HasPrefix(data.Message, errPrefix) {
|
||||
data.Message = strings.TrimLeft(data.Message, errPrefix)
|
||||
data.SpinnerClass = spinnerClassErrorSign
|
||||
} else {
|
||||
data.SpinnerClass = spinnerClassSpinner
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(make([]byte, 128)) // more than enough
|
||||
err := loadingPageTmpl.Execute(buf, data)
|
||||
if err != nil { // should never happen
|
||||
if err != nil { // should never happen in production
|
||||
panic(err)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
Header: http.Header{
|
||||
headerContentType: {htmlContentType},
|
||||
"Cache-Control": {
|
||||
"no-cache",
|
||||
"no-store",
|
||||
"must-revalidate",
|
||||
},
|
||||
},
|
||||
Body: io.NopCloser(buf),
|
||||
ContentLength: int64(buf.Len()),
|
||||
}, nil
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type (
|
||||
roundTripper struct {
|
||||
patched roundTripFunc
|
||||
}
|
||||
roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
)
|
||||
|
||||
func (rt roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return rt.patched(req)
|
||||
}
|
||||
|
||||
func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*http.Response, error) {
|
||||
// wake the container
|
||||
select {
|
||||
case w.wakeCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
// target site is ready, passthrough
|
||||
if w.ready.Load() {
|
||||
return origRoundTrip(req)
|
||||
}
|
||||
|
||||
// initial request
|
||||
targetUrl := req.Header.Get(headerGoProxyTargetURL)
|
||||
if targetUrl == "" {
|
||||
return w.makeResp(
|
||||
"%s is starting... Please wait",
|
||||
w.ContainerName,
|
||||
)
|
||||
}
|
||||
|
||||
w.l.Debug("serving event")
|
||||
|
||||
// stream request
|
||||
rtDone := make(chan *http.Response, 1)
|
||||
ctx, cancel := context.WithTimeout(req.Context(), w.WakeTimeout)
|
||||
defer cancel()
|
||||
|
||||
// loop original round trip until success in a goroutine
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
default:
|
||||
resp, err := origRoundTrip(req)
|
||||
if err == nil {
|
||||
w.ready.Store(true)
|
||||
rtDone <- resp
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
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)
|
||||
}
|
||||
return w.makeErrResp("idlewatcher has stopped\n%s", w.ctx.Err().Error())
|
||||
case <-w.ctx.Done():
|
||||
return w.makeErrResp("idlewatcher has stopped\n%s", w.ctx.Err().Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
136
internal/docker/idlewatcher/waker.go
Normal file
136
internal/docker/idlewatcher/waker.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
)
|
||||
|
||||
type Waker struct {
|
||||
*watcher
|
||||
|
||||
client *http.Client
|
||||
rp *gphttp.ReverseProxy
|
||||
}
|
||||
|
||||
func NewWaker(w *watcher, rp *gphttp.ReverseProxy) *Waker {
|
||||
tr := &http.Transport{}
|
||||
if w.NoTLSVerify {
|
||||
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
orig := rp.ServeHTTP
|
||||
// workaround for stopped containers port become zero
|
||||
rp.ServeHTTP = func(rw http.ResponseWriter, r *http.Request) {
|
||||
if rp.TargetURL.Port() == "0" {
|
||||
port, ok := portHistoryMap.Load(w.Alias)
|
||||
if !ok {
|
||||
w.l.Errorf("port history not found for %s", w.Alias)
|
||||
http.Error(rw, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rp.TargetURL.Host = fmt.Sprintf("%s:%v", rp.TargetURL.Hostname(), port)
|
||||
}
|
||||
orig(rw, r)
|
||||
}
|
||||
return &Waker{
|
||||
watcher: w,
|
||||
client: &http.Client{
|
||||
Timeout: 1 * time.Second,
|
||||
Transport: tr,
|
||||
},
|
||||
rp: rp,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Waker) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
w.wake(w.rp.ServeHTTP, rw, r)
|
||||
}
|
||||
|
||||
func (w *Waker) wake(next http.HandlerFunc, rw http.ResponseWriter, r *http.Request) {
|
||||
w.resetIdleTimer()
|
||||
|
||||
// pass through if container is ready
|
||||
if w.ready.Load() {
|
||||
next(rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), w.WakeTimeout)
|
||||
defer cancel()
|
||||
|
||||
accept := gphttp.GetAccept(r.Header)
|
||||
acceptHTML := r.Method == http.MethodGet && accept.AcceptHTML()
|
||||
|
||||
isCheckRedirect := r.Header.Get(headerCheckRedirect) != ""
|
||||
if !isCheckRedirect && acceptHTML {
|
||||
// Send a loading response to the client
|
||||
body := w.makeRespBody("%s waking up...", w.ContainerName)
|
||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
rw.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
||||
rw.Header().Add("Cache-Control", "no-cache")
|
||||
rw.Header().Add("Cache-Control", "no-store")
|
||||
rw.Header().Add("Cache-Control", "must-revalidate")
|
||||
rw.Write(body)
|
||||
return
|
||||
}
|
||||
|
||||
// wake the container and reset idle timer
|
||||
// also wait for another wake request
|
||||
w.wakeCh <- struct{}{}
|
||||
|
||||
if <-w.wakeDone != nil {
|
||||
http.Error(rw, "Error sending wake request", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// maybe another request came in while we were waiting for the wake
|
||||
if w.ready.Load() {
|
||||
if isCheckRedirect {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
next(rw, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
http.Error(rw, "Waking timed out", http.StatusGatewayTimeout)
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
wakeReq, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodHead,
|
||||
w.URL.String(),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
w.l.Errorf("new request err to %s: %s", r.URL, err)
|
||||
http.Error(rw, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
wakeResp, err := w.client.Do(wakeReq)
|
||||
if err == nil && wakeResp.StatusCode != http.StatusServiceUnavailable {
|
||||
w.ready.Store(true)
|
||||
w.l.Debug("awaken")
|
||||
if isCheckRedirect {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
next(rw, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// retry until the container is ready or timeout
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package idlewatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
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"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ type (
|
||||
|
||||
wakeCh chan struct{}
|
||||
wakeDone chan E.NestedError
|
||||
ticker *time.Ticker
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@@ -45,9 +46,11 @@ var (
|
||||
mainLoopCancel context.CancelFunc
|
||||
mainLoopWg sync.WaitGroup
|
||||
|
||||
watcherMap = make(map[string]*watcher)
|
||||
watcherMap = F.NewMapOf[string, *watcher]()
|
||||
watcherMapMu sync.Mutex
|
||||
|
||||
portHistoryMap = F.NewMapOf[PT.Alias, string]()
|
||||
|
||||
newWatcherCh = make(chan *watcher)
|
||||
|
||||
logger = logrus.WithField("module", "idle_watcher")
|
||||
@@ -63,7 +66,13 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||
watcherMapMu.Lock()
|
||||
defer watcherMapMu.Unlock()
|
||||
|
||||
if w, ok := watcherMap[entry.ContainerName]; ok {
|
||||
key := entry.ContainerID
|
||||
|
||||
if entry.URL.Port() != "0" {
|
||||
portHistoryMap.Store(entry.Alias, entry.URL.Port())
|
||||
}
|
||||
|
||||
if w, ok := watcherMap.Load(key); ok {
|
||||
w.refCount.Add(1)
|
||||
w.ReverseProxyEntry = entry
|
||||
return w, nil
|
||||
@@ -78,14 +87,15 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||
ReverseProxyEntry: entry,
|
||||
client: client,
|
||||
refCount: &sync.WaitGroup{},
|
||||
wakeCh: make(chan struct{}),
|
||||
wakeCh: make(chan struct{}, 1),
|
||||
wakeDone: make(chan E.NestedError),
|
||||
ticker: time.NewTicker(entry.IdleTimeout),
|
||||
l: logger.WithField("container", entry.ContainerName),
|
||||
}
|
||||
w.refCount.Add(1)
|
||||
w.stopByMethod = w.getStopCallback()
|
||||
|
||||
watcherMap[w.ContainerName] = w
|
||||
watcherMap.Store(key, w)
|
||||
|
||||
go func() {
|
||||
newWatcherCh <- w
|
||||
@@ -94,10 +104,8 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func Unregister(containerName string) {
|
||||
if w, ok := watcherMap[containerName]; ok {
|
||||
w.refCount.Add(-1)
|
||||
}
|
||||
func (w *watcher) Unregister() {
|
||||
w.refCount.Add(-1)
|
||||
}
|
||||
|
||||
func Start() {
|
||||
@@ -117,8 +125,7 @@ func Start() {
|
||||
w.watchUntilCancel()
|
||||
w.refCount.Wait() // wait for 0 ref count
|
||||
|
||||
w.client.Close()
|
||||
delete(watcherMap, w.ContainerName)
|
||||
watcherMap.Delete(w.ContainerID)
|
||||
w.l.Debug("unregistered")
|
||||
mainLoopWg.Done()
|
||||
}()
|
||||
@@ -131,36 +138,30 @@ func Stop() {
|
||||
mainLoopWg.Wait()
|
||||
}
|
||||
|
||||
func (w *watcher) PatchRoundTripper(rtp http.RoundTripper) roundTripper {
|
||||
return roundTripper{patched: func(r *http.Request) (*http.Response, error) {
|
||||
return w.roundTrip(rtp.RoundTrip, r)
|
||||
}}
|
||||
}
|
||||
|
||||
func (w *watcher) containerStop() error {
|
||||
return w.client.ContainerStop(w.ctx, w.ContainerName, container.StopOptions{
|
||||
return w.client.ContainerStop(w.ctx, w.ContainerID, container.StopOptions{
|
||||
Signal: string(w.StopSignal),
|
||||
Timeout: &w.StopTimeout})
|
||||
}
|
||||
|
||||
func (w *watcher) containerPause() error {
|
||||
return w.client.ContainerPause(w.ctx, w.ContainerName)
|
||||
return w.client.ContainerPause(w.ctx, w.ContainerID)
|
||||
}
|
||||
|
||||
func (w *watcher) containerKill() error {
|
||||
return w.client.ContainerKill(w.ctx, w.ContainerName, string(w.StopSignal))
|
||||
return w.client.ContainerKill(w.ctx, w.ContainerID, string(w.StopSignal))
|
||||
}
|
||||
|
||||
func (w *watcher) containerUnpause() error {
|
||||
return w.client.ContainerUnpause(w.ctx, w.ContainerName)
|
||||
return w.client.ContainerUnpause(w.ctx, w.ContainerID)
|
||||
}
|
||||
|
||||
func (w *watcher) containerStart() error {
|
||||
return w.client.ContainerStart(w.ctx, w.ContainerName, container.StartOptions{})
|
||||
return w.client.ContainerStart(w.ctx, w.ContainerID, container.StartOptions{})
|
||||
}
|
||||
|
||||
func (w *watcher) containerStatus() (string, E.NestedError) {
|
||||
json, err := w.client.ContainerInspect(w.ctx, w.ContainerName)
|
||||
json, err := w.client.ContainerInspect(w.ctx, w.ContainerID)
|
||||
if err != nil {
|
||||
return "", E.FailWith("inspect container", err)
|
||||
}
|
||||
@@ -168,6 +169,10 @@ func (w *watcher) containerStatus() (string, E.NestedError) {
|
||||
}
|
||||
|
||||
func (w *watcher) wakeIfStopped() E.NestedError {
|
||||
if w.ready.Load() || w.ContainerRunning {
|
||||
return nil
|
||||
}
|
||||
|
||||
status, err := w.containerStatus()
|
||||
|
||||
if err.HasError() {
|
||||
@@ -210,16 +215,20 @@ func (w *watcher) getStopCallback() StopCallback {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) resetIdleTimer() {
|
||||
w.ticker.Reset(w.IdleTimeout)
|
||||
}
|
||||
|
||||
func (w *watcher) watchUntilCancel() {
|
||||
defer close(w.wakeCh)
|
||||
|
||||
w.ctx, w.cancel = context.WithCancel(context.Background())
|
||||
w.ctx, w.cancel = context.WithCancel(mainLoopCtx)
|
||||
|
||||
dockerWatcher := W.NewDockerWatcherWithClient(w.client)
|
||||
dockerEventCh, dockerEventErrCh := dockerWatcher.EventsWithOptions(w.ctx, W.DockerListOptions{
|
||||
Filters: W.NewDockerFilter(
|
||||
W.DockerFilterContainer,
|
||||
W.DockerrFilterContainerName(w.ContainerName),
|
||||
W.DockerrFilterContainer(w.ContainerID),
|
||||
W.DockerFilterStart,
|
||||
W.DockerFilterStop,
|
||||
W.DockerFilterDie,
|
||||
@@ -228,14 +237,11 @@ func (w *watcher) watchUntilCancel() {
|
||||
W.DockerFilterUnpause,
|
||||
),
|
||||
})
|
||||
|
||||
ticker := time.NewTicker(w.IdleTimeout)
|
||||
defer ticker.Stop()
|
||||
defer w.ticker.Stop()
|
||||
defer w.client.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-mainLoopCtx.Done():
|
||||
w.cancel()
|
||||
case <-w.ctx.Done():
|
||||
w.l.Debug("stopped")
|
||||
return
|
||||
@@ -247,30 +253,29 @@ func (w *watcher) watchUntilCancel() {
|
||||
switch {
|
||||
// create / start / unpause
|
||||
case e.Action.IsContainerWake():
|
||||
ticker.Reset(w.IdleTimeout)
|
||||
w.ContainerRunning = true
|
||||
w.resetIdleTimer()
|
||||
w.l.Info(e)
|
||||
default: // stop / pause / kill
|
||||
ticker.Stop()
|
||||
default: // stop / pause / kil
|
||||
w.ContainerRunning = false
|
||||
w.ticker.Stop()
|
||||
w.ready.Store(false)
|
||||
w.l.Info(e)
|
||||
}
|
||||
case <-ticker.C:
|
||||
case <-w.ticker.C:
|
||||
w.l.Debug("idle timeout")
|
||||
ticker.Stop()
|
||||
w.ticker.Stop()
|
||||
if err := w.stopByMethod(); err != nil && err.IsNot(context.Canceled) {
|
||||
w.l.Error(E.FailWith("stop", err).Extraf("stop method: %s", w.StopMethod))
|
||||
}
|
||||
case <-w.wakeCh:
|
||||
w.l.Debug("wake signal received")
|
||||
ticker.Reset(w.IdleTimeout)
|
||||
w.resetIdleTimer()
|
||||
err := w.wakeIfStopped()
|
||||
if err != nil && err.IsNot(context.Canceled) {
|
||||
if err != nil {
|
||||
w.l.Error(E.FailWith("wake", err))
|
||||
}
|
||||
select {
|
||||
case w.wakeDone <- err: // this is passed to roundtrip
|
||||
default:
|
||||
}
|
||||
w.wakeDone <- err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -23,8 +22,6 @@ type (
|
||||
Value any
|
||||
}
|
||||
NestedLabelMap map[string]U.SerializedObject
|
||||
ValueParser func(string) (any, E.NestedError)
|
||||
ValueParserMap map[string]ValueParser
|
||||
)
|
||||
|
||||
func (l *Label) String() string {
|
||||
@@ -61,7 +58,14 @@ func ApplyLabel[T any](obj *T, l *Label) E.NestedError {
|
||||
}
|
||||
dst, ok := field.Interface().(NestedLabelMap)
|
||||
if !ok {
|
||||
return E.Invalid("type", field.Type())
|
||||
if field.Kind() == reflect.Ptr {
|
||||
if field.IsNil() {
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
}
|
||||
} else {
|
||||
field = field.Addr()
|
||||
}
|
||||
return U.Deserialize(U.SerializedObject{nestedLabel.Namespace: nestedLabel.Value}, field.Interface())
|
||||
}
|
||||
if dst == nil {
|
||||
field.Set(reflect.MakeMap(reflect.TypeFor[NestedLabelMap]()))
|
||||
@@ -107,45 +111,5 @@ func ParseLabel(label string, value string) (*Label, E.NestedError) {
|
||||
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]()
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
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)
|
||||
// }
|
||||
@@ -8,11 +8,15 @@ import (
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func makeLabel(ns, name, attr string) string {
|
||||
return fmt.Sprintf("%s.%s.%s", ns, name, attr)
|
||||
}
|
||||
|
||||
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)
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
|
||||
ExpectNoError(t, err.Error())
|
||||
sGot := ExpectType[*Label](t, pl.Value)
|
||||
ExpectFalse(t, sGot == nil)
|
||||
@@ -27,7 +31,7 @@ func TestApplyNestedLabel(t *testing.T) {
|
||||
mName := "middleware1"
|
||||
mAttr := "prop1"
|
||||
v := "value1"
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s.%s", ProxyAttributeMiddlewares, mName, mAttr)), v)
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
|
||||
ExpectNoError(t, err.Error())
|
||||
err = ApplyLabel(entry, pl)
|
||||
ExpectNoError(t, err.Error())
|
||||
@@ -51,7 +55,7 @@ func TestApplyNestedLabelExisting(t *testing.T) {
|
||||
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)
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
|
||||
ExpectNoError(t, err.Error())
|
||||
err = ApplyLabel(entry, pl)
|
||||
ExpectNoError(t, err.Error())
|
||||
@@ -76,7 +80,7 @@ func TestApplyNestedLabelNoAttr(t *testing.T) {
|
||||
entry.Middlewares = make(NestedLabelMap)
|
||||
entry.Middlewares[mName] = make(U.SerializedObject)
|
||||
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s", ProxyAttributeMiddlewares, mName)), v)
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s", "middlewares", mName)), v)
|
||||
ExpectNoError(t, err.Error())
|
||||
err = ApplyLabel(entry, pl)
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
@@ -3,6 +3,9 @@ package docker
|
||||
const (
|
||||
WildcardAlias = "*"
|
||||
|
||||
NSProxy = "proxy"
|
||||
NSHomePage = "homepage"
|
||||
|
||||
LabelAliases = NSProxy + ".aliases"
|
||||
LabelExclude = NSProxy + ".exclude"
|
||||
LabelIdleTimeout = NSProxy + ".idle_timeout"
|
||||
|
||||
@@ -6,6 +6,7 @@ type PortMapping = map[string]types.Port
|
||||
type ProxyProperties struct {
|
||||
DockerHost string `yaml:"-" json:"docker_host"`
|
||||
ContainerName string `yaml:"-" json:"container_name"`
|
||||
ContainerID string `yaml:"-" json:"container_id"`
|
||||
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
|
||||
@@ -14,6 +15,7 @@ type ProxyProperties struct {
|
||||
Aliases []string `yaml:"-" json:"aliases"`
|
||||
IsExcluded bool `yaml:"-" json:"is_excluded"`
|
||||
IsExplicit bool `yaml:"-" json:"is_explicit"`
|
||||
IsDatabase bool `yaml:"-" json:"is_database"`
|
||||
IdleTimeout string `yaml:"-" json:"idle_timeout"`
|
||||
WakeTimeout string `yaml:"-" json:"wake_timeout"`
|
||||
StopMethod string `yaml:"-" json:"stop_method"`
|
||||
|
||||
@@ -2,6 +2,7 @@ package error
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -24,7 +25,6 @@ func NewBuilder(format string, args ...any) Builder {
|
||||
func (b Builder) Add(err NestedError) Builder {
|
||||
if err != nil {
|
||||
b.Lock()
|
||||
// TODO: if err severity is higher than b.severity, update b.severity
|
||||
b.errors = append(b.errors, err)
|
||||
b.Unlock()
|
||||
}
|
||||
@@ -49,8 +49,8 @@ func (b Builder) Addf(format string, args ...any) Builder {
|
||||
func (b Builder) Build() NestedError {
|
||||
if len(b.errors) == 0 {
|
||||
return nil
|
||||
} else if len(b.errors) == 1 {
|
||||
return b.errors[0]
|
||||
} else if len(b.errors) == 1 && !strings.ContainsRune(b.message, ' ') {
|
||||
return b.errors[0].Subject(b.message)
|
||||
}
|
||||
return Join(b.message, b.errors...)
|
||||
}
|
||||
|
||||
@@ -166,6 +166,8 @@ func (ne NestedError) Subject(s any) NestedError {
|
||||
}
|
||||
if ne.subject == "" {
|
||||
ne.subject = subject
|
||||
} else if !strings.ContainsRune(subject, ' ') || strings.ContainsRune(ne.subject, '.') {
|
||||
ne.subject = fmt.Sprintf("%s.%s", subject, ne.subject)
|
||||
} else {
|
||||
ne.subject = fmt.Sprintf("%s > %s", subject, ne.subject)
|
||||
}
|
||||
@@ -182,8 +184,7 @@ func (ne NestedError) Subjectf(format string, args ...any) NestedError {
|
||||
if strings.Contains(format, "%w") {
|
||||
panic("Subjectf format should not contain %w")
|
||||
}
|
||||
ne.subject = fmt.Sprintf(format, args...)
|
||||
return ne
|
||||
return ne.Subject(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (ne NestedError) JSONObject() jsonNestedError {
|
||||
|
||||
@@ -2,17 +2,20 @@ package error
|
||||
|
||||
import (
|
||||
stderrors "errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
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")
|
||||
ErrDuplicated = stderrors.New("duplicated")
|
||||
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")
|
||||
ErrTypeError = stderrors.New("type error")
|
||||
ErrTypeMismatch = stderrors.New("type mismatch")
|
||||
)
|
||||
|
||||
const fmtSubjectWhat = "%w %v: %q"
|
||||
@@ -57,6 +60,18 @@ func Duplicated(subject, what any) NestedError {
|
||||
return errorf("%w %v: %v", ErrDuplicated, subject, what)
|
||||
}
|
||||
|
||||
func OutOfRange(subject string, value any) NestedError {
|
||||
func OutOfRange(subject any, value any) NestedError {
|
||||
return errorf("%v %w: %v", subject, ErrOutOfRange, value)
|
||||
}
|
||||
|
||||
func TypeError(subject any, from, to reflect.Type) NestedError {
|
||||
return errorf("%v %w: %s -> %s\n", subject, ErrTypeError, from, to)
|
||||
}
|
||||
|
||||
func TypeError2(subject any, from, to reflect.Value) NestedError {
|
||||
return TypeError(subject, from.Type(), to.Type())
|
||||
}
|
||||
|
||||
func TypeMismatch[Expect any](value any) NestedError {
|
||||
return errorf("%w: expect %s got %T", ErrTypeMismatch, reflect.TypeFor[Expect](), value)
|
||||
}
|
||||
|
||||
43
internal/homepage/homepage.go
Normal file
43
internal/homepage/homepage.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package homepage
|
||||
|
||||
type (
|
||||
HomePageConfig map[string]HomePageCategory
|
||||
HomePageCategory []*HomePageItem
|
||||
|
||||
HomePageItem struct {
|
||||
Show bool `yaml:"show" json:"show"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Icon string `yaml:"icon" json:"icon"`
|
||||
URL string `yaml:"url" json:"url"` // alias + domain
|
||||
Category string `yaml:"category" json:"category"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
WidgetConfig map[string]any `yaml:",flow" json:"widget_config"`
|
||||
|
||||
SourceType string `yaml:"-" json:"source_type"`
|
||||
AltURL string `yaml:"-" json:"alt_url"` // original proxy target
|
||||
}
|
||||
)
|
||||
|
||||
func (item *HomePageItem) IsEmpty() bool {
|
||||
return item == nil || (item.Name == "" &&
|
||||
item.Icon == "" &&
|
||||
item.URL == "" &&
|
||||
item.Category == "" &&
|
||||
item.Description == "" &&
|
||||
len(item.WidgetConfig) == 0)
|
||||
}
|
||||
|
||||
func NewHomePageConfig() HomePageConfig {
|
||||
return HomePageConfig(make(map[string]HomePageCategory))
|
||||
}
|
||||
|
||||
func (c *HomePageConfig) Clear() {
|
||||
*c = make(HomePageConfig)
|
||||
}
|
||||
|
||||
func (c HomePageConfig) Add(item *HomePageItem) {
|
||||
if c[item.Category] == nil {
|
||||
c[item.Category] = make(HomePageCategory, 0)
|
||||
}
|
||||
c[item.Category] = append(c[item.Category], item)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
99
internal/list-icons.go
Normal file
99
internal/list-icons.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"log"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type GitHubContents struct { //! keep this, may reuse in future
|
||||
Type string `json:"type"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Sha string `json:"sha"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
const iconsCachePath = "/tmp/icons_cache.json"
|
||||
const updateInterval = 1 * time.Hour
|
||||
|
||||
func ListAvailableIcons() ([]string, error) {
|
||||
owner := "walkxcode"
|
||||
repo := "dashboard-icons"
|
||||
ref := "main"
|
||||
|
||||
var lastUpdate time.Time
|
||||
var icons = make([]string, 0)
|
||||
info, err := os.Stat(iconsCachePath)
|
||||
if err == nil {
|
||||
lastUpdate = info.ModTime().Local()
|
||||
}
|
||||
if time.Since(lastUpdate) < updateInterval {
|
||||
err := utils.LoadJson(iconsCachePath, &icons)
|
||||
if err == nil {
|
||||
return icons, nil
|
||||
}
|
||||
}
|
||||
|
||||
contents, err := getRepoContents(http.DefaultClient, owner, repo, ref, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, content := range contents {
|
||||
if content.Type != "dir" {
|
||||
icons = append(icons, content.Path)
|
||||
}
|
||||
}
|
||||
err = utils.SaveJson(iconsCachePath, &icons, 0o644).Error()
|
||||
if err != nil {
|
||||
log.Print("error saving cache", err)
|
||||
}
|
||||
return icons, nil
|
||||
}
|
||||
|
||||
func getRepoContents(client *http.Client, owner string, repo string, ref string, path string) ([]GitHubContents, error) {
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s?ref=%s", owner, repo, path, ref), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var contents []GitHubContents
|
||||
err = json.Unmarshal(body, &contents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filesAndDirs := make([]GitHubContents, 0)
|
||||
for _, content := range contents {
|
||||
if content.Type == "dir" {
|
||||
subContents, err := getRepoContents(client, owner, repo, ref, content.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filesAndDirs = append(filesAndDirs, subContents...)
|
||||
} else {
|
||||
filesAndDirs = append(filesAndDirs, content)
|
||||
}
|
||||
}
|
||||
|
||||
return filesAndDirs, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package common
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@@ -13,9 +13,13 @@ var (
|
||||
KeepAlive: 60 * time.Second,
|
||||
}
|
||||
DefaultTransport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: defaultDialer.DialContext,
|
||||
MaxIdleConnsPerHost: 1000,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: defaultDialer.DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
DefaultTransportNoTLS = func() *http.Transport {
|
||||
var clone = DefaultTransport.Clone()
|
||||
76
internal/net/http/content_type.go
Normal file
76
internal/net/http/content_type.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ContentType string
|
||||
type AcceptContentType []ContentType
|
||||
|
||||
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 GetAccept(h http.Header) AcceptContentType {
|
||||
var accepts []ContentType
|
||||
for _, v := range h["Accept"] {
|
||||
ct, _, err := mime.ParseMediaType(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
accepts = append(accepts, ContentType(ct))
|
||||
}
|
||||
return accepts
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
func (act AcceptContentType) IsEmpty() bool {
|
||||
return len(act) == 0
|
||||
}
|
||||
|
||||
func (act AcceptContentType) AcceptHTML() bool {
|
||||
for _, v := range act {
|
||||
if v.IsHTML() || v == "text/*" || v == "*/*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (act AcceptContentType) AcceptJSON() bool {
|
||||
for _, v := range act {
|
||||
if v.IsJSON() || v == "*/*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (act AcceptContentType) AcceptPlainText() bool {
|
||||
for _, v := range act {
|
||||
if v.IsPlainText() || v == "text/*" || v == "*/*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
41
internal/net/http/content_type_test.go
Normal file
41
internal/net/http/content_type_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestContentTypes(t *testing.T) {
|
||||
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"text/html"}}).IsHTML())
|
||||
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"text/html; charset=utf-8"}}).IsHTML())
|
||||
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"application/xhtml+xml"}}).IsHTML())
|
||||
ExpectFalse(t, GetContentType(http.Header{"Content-Type": {"text/plain"}}).IsHTML())
|
||||
|
||||
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"application/json"}}).IsJSON())
|
||||
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"application/json; charset=utf-8"}}).IsJSON())
|
||||
ExpectFalse(t, GetContentType(http.Header{"Content-Type": {"text/html"}}).IsJSON())
|
||||
|
||||
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"text/plain"}}).IsPlainText())
|
||||
ExpectTrue(t, GetContentType(http.Header{"Content-Type": {"text/plain; charset=utf-8"}}).IsPlainText())
|
||||
ExpectFalse(t, GetContentType(http.Header{"Content-Type": {"text/html"}}).IsPlainText())
|
||||
}
|
||||
|
||||
func TestAcceptContentTypes(t *testing.T) {
|
||||
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/html", "text/plain"}}).AcceptPlainText())
|
||||
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/html", "text/plain; charset=utf-8"}}).AcceptPlainText())
|
||||
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/html", "text/plain"}}).AcceptHTML())
|
||||
ExpectTrue(t, GetAccept(http.Header{"Accept": {"application/json"}}).AcceptJSON())
|
||||
ExpectTrue(t, GetAccept(http.Header{"Accept": {"*/*"}}).AcceptPlainText())
|
||||
ExpectTrue(t, GetAccept(http.Header{"Accept": {"*/*"}}).AcceptHTML())
|
||||
ExpectTrue(t, GetAccept(http.Header{"Accept": {"*/*"}}).AcceptJSON())
|
||||
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/*"}}).AcceptPlainText())
|
||||
ExpectTrue(t, GetAccept(http.Header{"Accept": {"text/*"}}).AcceptHTML())
|
||||
|
||||
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/plain"}}).AcceptHTML())
|
||||
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/plain; charset=utf-8"}}).AcceptHTML())
|
||||
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/html"}}).AcceptPlainText())
|
||||
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/html"}}).AcceptJSON())
|
||||
ExpectFalse(t, GetAccept(http.Header{"Accept": {"text/*"}}).AcceptJSON())
|
||||
}
|
||||
53
internal/net/http/header_utils.go
Normal file
53
internal/net/http/header_utils.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
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) http.Header {
|
||||
if len(allowed) == 0 {
|
||||
return h
|
||||
}
|
||||
|
||||
filtered := make(http.Header)
|
||||
|
||||
for i, header := range allowed {
|
||||
values := h.Values(header)
|
||||
if len(values) == 0 {
|
||||
continue
|
||||
}
|
||||
filtered[http.CanonicalHeaderKey(allowed[i])] = append([]string(nil), values...)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func HeaderToMap(h http.Header) map[string]string {
|
||||
result := make(map[string]string)
|
||||
for k, v := range h {
|
||||
if len(v) > 0 {
|
||||
result[k] = v[0] // Take the first value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
83
internal/net/http/middleware/cidr_whitelist.go
Normal file
83
internal/net/http/middleware/cidr_whitelist.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
)
|
||||
|
||||
type cidrWhitelist struct {
|
||||
*cidrWhitelistOpts
|
||||
m *Middleware
|
||||
}
|
||||
|
||||
type cidrWhitelistOpts struct {
|
||||
Allow []*types.CIDR
|
||||
StatusCode int
|
||||
Message string
|
||||
|
||||
cachedAddr F.Map[string, bool] // cache for trusted IPs
|
||||
}
|
||||
|
||||
var CIDRWhiteList = &cidrWhitelist{
|
||||
m: &Middleware{withOptions: NewCIDRWhitelist},
|
||||
}
|
||||
|
||||
var cidrWhitelistDefaults = func() *cidrWhitelistOpts {
|
||||
return &cidrWhitelistOpts{
|
||||
Allow: []*types.CIDR{},
|
||||
StatusCode: http.StatusForbidden,
|
||||
Message: "IP not allowed",
|
||||
cachedAddr: F.NewMapOf[string, bool](),
|
||||
}
|
||||
}
|
||||
|
||||
func NewCIDRWhitelist(opts OptionsRaw) (*Middleware, E.NestedError) {
|
||||
wl := new(cidrWhitelist)
|
||||
wl.m = &Middleware{
|
||||
impl: wl,
|
||||
before: wl.checkIP,
|
||||
}
|
||||
wl.cidrWhitelistOpts = cidrWhitelistDefaults()
|
||||
err := Deserialize(opts, wl.cidrWhitelistOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(wl.cidrWhitelistOpts.Allow) == 0 {
|
||||
return nil, E.Missing("allow range")
|
||||
}
|
||||
return wl.m, nil
|
||||
}
|
||||
|
||||
func (wl *cidrWhitelist) checkIP(next http.HandlerFunc, w ResponseWriter, r *Request) {
|
||||
var allow, ok bool
|
||||
if allow, ok = wl.cachedAddr.Load(r.RemoteAddr); !ok {
|
||||
ipStr, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
ipStr = r.RemoteAddr
|
||||
}
|
||||
ip := net.ParseIP(ipStr)
|
||||
for _, cidr := range wl.cidrWhitelistOpts.Allow {
|
||||
if cidr.Contains(ip) {
|
||||
wl.cachedAddr.Store(r.RemoteAddr, true)
|
||||
allow = true
|
||||
wl.m.AddTracef("client %s is allowed", ipStr).With("allowed CIDR", cidr)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allow {
|
||||
wl.cachedAddr.Store(r.RemoteAddr, false)
|
||||
wl.m.AddTracef("client %s is forbidden", ipStr).With("allowed CIDRs", wl.cidrWhitelistOpts.Allow)
|
||||
}
|
||||
}
|
||||
if !allow {
|
||||
w.WriteHeader(wl.StatusCode)
|
||||
w.Write([]byte(wl.Message))
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
42
internal/net/http/middleware/cidr_whitelist_test.go
Normal file
42
internal/net/http/middleware/cidr_whitelist_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
//go:embed test_data/cidr_whitelist_test.yml
|
||||
var testCIDRWhitelistCompose []byte
|
||||
var deny, accept *Middleware
|
||||
|
||||
func TestCIDRWhitelist(t *testing.T) {
|
||||
mids, err := BuildMiddlewaresFromYAML(testCIDRWhitelistCompose)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
deny = mids["deny@file"]
|
||||
accept = mids["accept@file"]
|
||||
if deny == nil || accept == nil {
|
||||
panic("bug occurred")
|
||||
}
|
||||
|
||||
t.Run("deny", func(t *testing.T) {
|
||||
for range 10 {
|
||||
result, err := newMiddlewareTest(deny, nil)
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectEqual(t, result.ResponseStatus, cidrWhitelistDefaults().StatusCode)
|
||||
ExpectEqual(t, string(result.Data), cidrWhitelistDefaults().Message)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("accept", func(t *testing.T) {
|
||||
for range 10 {
|
||||
result, err := newMiddlewareTest(accept, nil)
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectEqual(t, result.ResponseStatus, http.StatusOK)
|
||||
}
|
||||
})
|
||||
}
|
||||
118
internal/net/http/middleware/cloudflare_real_ip.go
Normal file
118
internal/net/http/middleware/cloudflare_real_ip.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
const (
|
||||
cfIPv4CIDRsEndpoint = "https://www.cloudflare.com/ips-v4"
|
||||
cfIPv6CIDRsEndpoint = "https://www.cloudflare.com/ips-v6"
|
||||
cfCIDRsUpdateInterval = time.Hour
|
||||
cfCIDRsUpdateRetryInterval = 3 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
cfCIDRsLastUpdate time.Time
|
||||
cfCIDRsMu sync.Mutex
|
||||
cfCIDRsLogger = logrus.WithField("middleware", "CloudflareRealIP")
|
||||
)
|
||||
|
||||
var CloudflareRealIP = &realIP{
|
||||
m: &Middleware{withOptions: NewCloudflareRealIP},
|
||||
}
|
||||
|
||||
func NewCloudflareRealIP(_ OptionsRaw) (*Middleware, E.NestedError) {
|
||||
cri := new(realIP)
|
||||
cri.m = &Middleware{
|
||||
impl: cri,
|
||||
before: func(next http.HandlerFunc, w ResponseWriter, r *Request) {
|
||||
cidrs := tryFetchCFCIDR()
|
||||
if cidrs != nil {
|
||||
cri.From = cidrs
|
||||
}
|
||||
cri.setRealIP(r)
|
||||
next(w, r)
|
||||
},
|
||||
}
|
||||
cri.realIPOpts = &realIPOpts{
|
||||
Header: "CF-Connecting-IP",
|
||||
Recursive: true,
|
||||
}
|
||||
return cri.m, nil
|
||||
}
|
||||
|
||||
func tryFetchCFCIDR() (cfCIDRs []*types.CIDR) {
|
||||
if time.Since(cfCIDRsLastUpdate) < cfCIDRsUpdateInterval {
|
||||
return
|
||||
}
|
||||
|
||||
cfCIDRsMu.Lock()
|
||||
defer cfCIDRsMu.Unlock()
|
||||
|
||||
if time.Since(cfCIDRsLastUpdate) < cfCIDRsUpdateInterval {
|
||||
return
|
||||
}
|
||||
|
||||
if common.IsTest {
|
||||
cfCIDRs = []*types.CIDR{
|
||||
{IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 0, 0, 0)},
|
||||
{IP: net.IPv4(10, 0, 0, 0), Mask: net.IPv4Mask(255, 0, 0, 0)},
|
||||
{IP: net.IPv4(172, 16, 0, 0), Mask: net.IPv4Mask(255, 255, 0, 0)},
|
||||
{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)},
|
||||
}
|
||||
} else {
|
||||
cfCIDRs = make([]*types.CIDR, 0, 30)
|
||||
err := errors.Join(
|
||||
fetchUpdateCFIPRange(cfIPv4CIDRsEndpoint, cfCIDRs),
|
||||
fetchUpdateCFIPRange(cfIPv6CIDRsEndpoint, cfCIDRs),
|
||||
)
|
||||
if err != nil {
|
||||
cfCIDRsLastUpdate = time.Now().Add(-cfCIDRsUpdateRetryInterval - cfCIDRsUpdateInterval)
|
||||
cfCIDRsLogger.Errorf("failed to update cloudflare range: %s, retry in %s", err, cfCIDRsUpdateRetryInterval)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
cfCIDRsLastUpdate = time.Now()
|
||||
cfCIDRsLogger.Debugf("cloudflare CIDR range updated")
|
||||
return
|
||||
}
|
||||
|
||||
func fetchUpdateCFIPRange(endpoint string, cfCIDRs []*types.CIDR) error {
|
||||
resp, err := http.Get(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(body), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
_, cidr, err := net.ParseCIDR(line)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cloudflare responeded an invalid CIDR: %s", line)
|
||||
} else {
|
||||
cfCIDRs = append(cfCIDRs, (*types.CIDR)(cidr))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -10,20 +10,19 @@ import (
|
||||
|
||||
"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"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
)
|
||||
|
||||
var CustomErrorPage = &Middleware{
|
||||
before: func(next http.Handler, w ResponseWriter, r *Request) {
|
||||
before: func(next http.HandlerFunc, w ResponseWriter, r *Request) {
|
||||
if !ServeStaticErrorPageFile(w, r) {
|
||||
next.ServeHTTP(w, r)
|
||||
next(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()) {
|
||||
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)
|
||||
@@ -47,8 +46,8 @@ func ServeStaticErrorPageFile(w http.ResponseWriter, r *http.Request) bool {
|
||||
if path != "" && path[0] != '/' {
|
||||
path = "/" + path
|
||||
}
|
||||
if strings.HasPrefix(path, common.StaticFilePathPrefix) {
|
||||
filename := path[len(common.StaticFilePathPrefix):]
|
||||
if strings.HasPrefix(path, gphttp.StaticFilePathPrefix) {
|
||||
filename := path[len(gphttp.StaticFilePathPrefix):]
|
||||
file, ok := error_page.GetStaticFile(filename)
|
||||
if !ok {
|
||||
errPageLogger.Errorf("unable to load resource %s", filename)
|
||||
@@ -13,12 +13,8 @@ import (
|
||||
"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"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -32,66 +28,51 @@ type (
|
||||
TrustForwardHeader bool
|
||||
AuthResponseHeaders []string
|
||||
AddAuthCookiesToResponse []string
|
||||
transport http.RoundTripper
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
var ForwardAuth = &forwardAuth{
|
||||
m: &Middleware{withOptions: NewForwardAuthfunc},
|
||||
}
|
||||
|
||||
func (fa *forwardAuth) forward(next http.Handler, w ResponseWriter, req *Request) {
|
||||
gpHTTP.RemoveHop(req.Header)
|
||||
func NewForwardAuthfunc(optsRaw OptionsRaw) (*Middleware, E.NestedError) {
|
||||
fa := new(forwardAuth)
|
||||
fa.forwardAuthOpts = new(forwardAuthOpts)
|
||||
err := Deserialize(optsRaw, fa.forwardAuthOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = E.Check(url.Parse(fa.Address))
|
||||
if err != nil {
|
||||
return nil, E.Invalid("address", fa.Address)
|
||||
}
|
||||
|
||||
fa.m = &Middleware{
|
||||
impl: fa,
|
||||
before: fa.forward,
|
||||
}
|
||||
|
||||
// TODO: use tr from reverse proxy
|
||||
tr, ok := fa.transport.(*http.Transport)
|
||||
if ok {
|
||||
tr = tr.Clone()
|
||||
} else {
|
||||
tr = gphttp.DefaultTransport.Clone()
|
||||
}
|
||||
|
||||
fa.client = http.Client{
|
||||
CheckRedirect: func(r *Request, via []*Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: tr,
|
||||
}
|
||||
return fa.m, nil
|
||||
}
|
||||
|
||||
func (fa *forwardAuth) forward(next http.HandlerFunc, w ResponseWriter, req *Request) {
|
||||
gphttp.RemoveHop(req.Header)
|
||||
|
||||
faReq, err := http.NewRequestWithContext(
|
||||
req.Context(),
|
||||
@@ -100,20 +81,21 @@ func (fa *forwardAuth) forward(next http.Handler, w ResponseWriter, req *Request
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
faLogger.Debugf("new request err to %s: %s", fa.Address, err)
|
||||
fa.m.AddTracef("new request err to %s", fa.Address).WithError(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
gpHTTP.CopyHeader(faReq.Header, req.Header)
|
||||
gpHTTP.RemoveHop(faReq.Header)
|
||||
gphttp.CopyHeader(faReq.Header, req.Header)
|
||||
gphttp.RemoveHop(faReq.Header)
|
||||
|
||||
gpHTTP.FilterHeaders(faReq.Header, fa.AuthResponseHeaders)
|
||||
faReq.Header = gphttp.FilterHeaders(faReq.Header, fa.AuthResponseHeaders)
|
||||
fa.setAuthHeaders(req, faReq)
|
||||
fa.m.AddTraceRequest("forward auth request", faReq)
|
||||
|
||||
faResp, err := fa.client.Do(faReq)
|
||||
if err != nil {
|
||||
faLogger.Debugf("failed to call %s: %s", fa.Address, err)
|
||||
fa.m.AddTracef("failed to call %s", fa.Address).WithError(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -121,28 +103,30 @@ func (fa *forwardAuth) forward(next http.Handler, w ResponseWriter, req *Request
|
||||
|
||||
body, err := io.ReadAll(faResp.Body)
|
||||
if err != nil {
|
||||
faLogger.Debugf("failed to read response body from %s: %s", fa.Address, err)
|
||||
fa.m.AddTracef("failed to read response body from %s", fa.Address).WithError(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())
|
||||
fa.m.AddTraceResponse("forward auth response", faResp)
|
||||
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)
|
||||
fa.m.AddTracef("failed to get location from %s", fa.Address).WithError(err).WithResponse(faResp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
} else if redirectURL.String() != "" {
|
||||
w.Header().Set("Location", redirectURL.String())
|
||||
fa.m.AddTracef("redirect to %q", redirectURL.String()).WithResponse(faResp)
|
||||
}
|
||||
|
||||
w.WriteHeader(faResp.StatusCode)
|
||||
|
||||
if _, err = w.Write(body); err != nil {
|
||||
faLogger.Debugf("failed to write response body from %s: %s", fa.Address, err)
|
||||
fa.m.AddTracef("failed to write response body from %s", fa.Address).WithError(err).WithResponse(faResp)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -164,7 +148,7 @@ func (fa *forwardAuth) forward(next http.Handler, w ResponseWriter, req *Request
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(gpHTTP.NewModifyResponseWriter(w, req, func(resp *Response) error {
|
||||
next.ServeHTTP(gphttp.NewModifyResponseWriter(w, req, func(resp *Response) error {
|
||||
fa.setAuthCookies(resp, authCookies)
|
||||
return nil
|
||||
}), req)
|
||||
155
internal/net/http/middleware/middleware.go
Normal file
155
internal/net/http/middleware/middleware.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
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.HandlerFunc, w ResponseWriter, r *Request)
|
||||
RewriteFunc func(req *Request)
|
||||
ModifyResponseFunc func(resp *Response) error
|
||||
CloneWithOptFunc func(opts OptionsRaw) (*Middleware, E.NestedError)
|
||||
|
||||
OptionsRaw = map[string]any
|
||||
Options any
|
||||
|
||||
Middleware struct {
|
||||
name string
|
||||
|
||||
before BeforeFunc // runs before ReverseProxy.ServeHTTP
|
||||
modifyResponse ModifyResponseFunc // runs after ReverseProxy.ModifyResponse
|
||||
|
||||
withOptions CloneWithOptFunc
|
||||
impl any
|
||||
|
||||
parent *Middleware
|
||||
children []*Middleware
|
||||
trace bool
|
||||
}
|
||||
)
|
||||
|
||||
var Deserialize = U.Deserialize
|
||||
|
||||
func Rewrite(r RewriteFunc) BeforeFunc {
|
||||
return func(next http.HandlerFunc, w ResponseWriter, req *Request) {
|
||||
r(req)
|
||||
next(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Middleware) Name() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
func (m *Middleware) Fullname() string {
|
||||
if m.parent != nil {
|
||||
return m.parent.Fullname() + "." + m.name
|
||||
}
|
||||
return m.name
|
||||
}
|
||||
|
||||
func (m *Middleware) String() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
func (m *Middleware) MarshalJSON() ([]byte, error) {
|
||||
return json.MarshalIndent(map[string]any{
|
||||
"name": m.name,
|
||||
"options": m.impl,
|
||||
}, "", " ")
|
||||
}
|
||||
|
||||
func (m *Middleware) WithOptionsClone(optsRaw OptionsRaw) (*Middleware, E.NestedError) {
|
||||
if len(optsRaw) != 0 && m.withOptions != nil {
|
||||
if mWithOpt, err := m.withOptions(optsRaw); 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.modifyResponse,
|
||||
nil,
|
||||
m.impl,
|
||||
m.parent,
|
||||
m.children,
|
||||
false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO: check conflict or duplicates
|
||||
func PatchReverseProxy(rpName string, rp *ReverseProxy, middlewaresMap map[string]OptionsRaw) (res E.NestedError) {
|
||||
middlewares := make([]*Middleware, 0, len(middlewaresMap))
|
||||
|
||||
invalidM := E.NewBuilder("invalid middlewares")
|
||||
invalidOpts := E.NewBuilder("invalid options")
|
||||
defer func() {
|
||||
invalidM.Add(invalidOpts.Build())
|
||||
invalidM.To(&res)
|
||||
}()
|
||||
|
||||
for name, opts := range middlewaresMap {
|
||||
m, ok := Get(name)
|
||||
if !ok {
|
||||
invalidM.Add(E.NotExist("middleware", name))
|
||||
continue
|
||||
}
|
||||
|
||||
m, err := m.WithOptionsClone(opts)
|
||||
if err != nil {
|
||||
invalidOpts.Add(err.Subject(name))
|
||||
continue
|
||||
}
|
||||
middlewares = append(middlewares, m)
|
||||
}
|
||||
|
||||
if invalidM.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
patchReverseProxy(rpName, rp, middlewares)
|
||||
return
|
||||
}
|
||||
|
||||
func patchReverseProxy(rpName string, rp *ReverseProxy, middlewares []*Middleware) {
|
||||
mid := BuildMiddlewareFromChain(rpName, middlewares)
|
||||
|
||||
if mid.before != nil {
|
||||
ori := rp.ServeHTTP
|
||||
rp.ServeHTTP = func(w http.ResponseWriter, r *http.Request) {
|
||||
mid.before(ori, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
if mid.modifyResponse != nil {
|
||||
if rp.ModifyResponse != nil {
|
||||
ori := rp.ModifyResponse
|
||||
rp.ModifyResponse = func(res *http.Response) error {
|
||||
return errors.Join(mid.modifyResponse(res), ori(res))
|
||||
}
|
||||
} else {
|
||||
rp.ModifyResponse = mid.modifyResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
114
internal/net/http/middleware/middleware_builder.go
Normal file
114
internal/net/http/middleware/middleware_builder.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func BuildMiddlewaresFromComposeFile(filePath string) (map[string]*Middleware, E.NestedError) {
|
||||
fileContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, E.FailWith("read middleware compose file", err)
|
||||
}
|
||||
return BuildMiddlewaresFromYAML(fileContent)
|
||||
}
|
||||
|
||||
func BuildMiddlewaresFromYAML(data []byte) (middlewares map[string]*Middleware, outErr E.NestedError) {
|
||||
b := E.NewBuilder("middlewares compile errors")
|
||||
defer b.To(&outErr)
|
||||
|
||||
var rawMap map[string][]map[string]any
|
||||
err := yaml.Unmarshal(data, &rawMap)
|
||||
if err != nil {
|
||||
b.Add(E.FailWith("yaml unmarshal", err))
|
||||
return
|
||||
}
|
||||
middlewares = make(map[string]*Middleware)
|
||||
for name, defs := range rawMap {
|
||||
chainErr := E.NewBuilder(name)
|
||||
chain := make([]*Middleware, 0, len(defs))
|
||||
for i, def := range defs {
|
||||
if def["use"] == nil || def["use"] == "" {
|
||||
chainErr.Add(E.Missing("use").Subjectf(".%d", i))
|
||||
continue
|
||||
}
|
||||
baseName := def["use"].(string)
|
||||
base, ok := Get(baseName)
|
||||
if !ok {
|
||||
base, ok = middlewares[baseName]
|
||||
if !ok {
|
||||
chainErr.Add(E.NotExist("middleware", baseName).Subjectf(".%d", i))
|
||||
continue
|
||||
}
|
||||
}
|
||||
delete(def, "use")
|
||||
m, err := base.WithOptionsClone(def)
|
||||
m.name = fmt.Sprintf("%s[%d]", name, i)
|
||||
if err != nil {
|
||||
chainErr.Add(err.Subjectf("item%d", i))
|
||||
continue
|
||||
}
|
||||
chain = append(chain, m)
|
||||
}
|
||||
if chainErr.HasError() {
|
||||
b.Add(chainErr.Build())
|
||||
} else {
|
||||
middlewares[name+"@file"] = BuildMiddlewareFromChain(name, chain)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: check conflict or duplicates
|
||||
func BuildMiddlewareFromChain(name string, chain []*Middleware) *Middleware {
|
||||
m := &Middleware{name: name, children: chain}
|
||||
|
||||
var befores []*Middleware
|
||||
var modResps []*Middleware
|
||||
|
||||
for _, comp := range chain {
|
||||
if comp.before != nil {
|
||||
befores = append(befores, comp)
|
||||
}
|
||||
if comp.modifyResponse != nil {
|
||||
modResps = append(modResps, comp)
|
||||
}
|
||||
comp.parent = m
|
||||
}
|
||||
|
||||
if len(befores) > 0 {
|
||||
m.before = buildBefores(befores)
|
||||
}
|
||||
if len(modResps) > 0 {
|
||||
m.modifyResponse = func(res *Response) error {
|
||||
b := E.NewBuilder("errors in middleware")
|
||||
for _, mr := range modResps {
|
||||
b.Add(E.From(mr.modifyResponse(res)).Subject(mr.name))
|
||||
}
|
||||
return b.Build().Error()
|
||||
}
|
||||
}
|
||||
|
||||
if common.IsDebug {
|
||||
m.EnableTrace()
|
||||
m.AddTracef("middleware created")
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func buildBefores(befores []*Middleware) BeforeFunc {
|
||||
if len(befores) == 1 {
|
||||
return befores[0].before
|
||||
}
|
||||
nextBefores := buildBefores(befores[1:])
|
||||
return func(next http.HandlerFunc, w ResponseWriter, r *Request) {
|
||||
befores[0].before(func(w ResponseWriter, r *Request) {
|
||||
nextBefores(next, w, r)
|
||||
}, w, r)
|
||||
}
|
||||
}
|
||||
22
internal/net/http/middleware/middleware_builder_test.go
Normal file
22
internal/net/http/middleware/middleware_builder_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
//go:embed test_data/middleware_compose.yml
|
||||
var testMiddlewareCompose []byte
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
middlewares, err := BuildMiddlewaresFromYAML(testMiddlewareCompose)
|
||||
ExpectNoError(t, err.Error())
|
||||
_, err = E.Check(json.MarshalIndent(middlewares, "", " "))
|
||||
ExpectNoError(t, err.Error())
|
||||
// t.Log(string(data))
|
||||
// TODO: test
|
||||
}
|
||||
78
internal/net/http/middleware/middlewares.go
Normal file
78
internal/net/http/middleware/middlewares.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
var middlewares map[string]*Middleware
|
||||
|
||||
func Get(name string) (middleware *Middleware, ok bool) {
|
||||
middleware, ok = middlewares[U.ToLowerNoSnake(name)]
|
||||
return
|
||||
}
|
||||
|
||||
func All() map[string]*Middleware {
|
||||
return middlewares
|
||||
}
|
||||
|
||||
// initialize middleware names and label parsers
|
||||
func init() {
|
||||
middlewares = map[string]*Middleware{
|
||||
"setxforwarded": SetXForwarded,
|
||||
"hidexforwarded": HideXForwarded,
|
||||
"redirecthttp": RedirectHTTP,
|
||||
"forwardauth": ForwardAuth.m,
|
||||
"modifyresponse": ModifyResponse.m,
|
||||
"modifyrequest": ModifyRequest.m,
|
||||
"errorpage": CustomErrorPage,
|
||||
"customerrorpage": CustomErrorPage,
|
||||
"realip": RealIP.m,
|
||||
"cloudflarerealip": CloudflareRealIP.m,
|
||||
"cidrwhitelist": CIDRWhiteList.m,
|
||||
}
|
||||
names := make(map[*Middleware][]string)
|
||||
for name, m := range middlewares {
|
||||
names[m] = append(names[m], http.CanonicalHeaderKey(name))
|
||||
}
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func LoadComposeFiles() {
|
||||
b := E.NewBuilder("failed to load middlewares")
|
||||
middlewareDefs, err := U.ListFiles(common.MiddlewareComposeBasePath, 0)
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to list middleware definitions: %s", err)
|
||||
return
|
||||
}
|
||||
for _, defFile := range middlewareDefs {
|
||||
mws, err := BuildMiddlewaresFromComposeFile(defFile)
|
||||
for name, m := range mws {
|
||||
if _, ok := middlewares[name]; ok {
|
||||
b.Add(E.Duplicated("middleware", name))
|
||||
continue
|
||||
}
|
||||
middlewares[U.ToLowerNoSnake(name)] = m
|
||||
logger.Infof("middleware %s loaded from %s", name, path.Base(defFile))
|
||||
}
|
||||
b.Add(err.Subject(path.Base(defFile)))
|
||||
}
|
||||
if b.HasError() {
|
||||
logger.Error(b.Build())
|
||||
}
|
||||
}
|
||||
|
||||
var logger = logrus.WithField("module", "middlewares")
|
||||
61
internal/net/http/middleware/modify_request.go
Normal file
61
internal/net/http/middleware/modify_request.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type (
|
||||
modifyRequest struct {
|
||||
*modifyRequestOpts
|
||||
m *Middleware
|
||||
}
|
||||
// order: set_headers -> add_headers -> hide_headers
|
||||
modifyRequestOpts struct {
|
||||
SetHeaders map[string]string
|
||||
AddHeaders map[string]string
|
||||
HideHeaders []string
|
||||
}
|
||||
)
|
||||
|
||||
var ModifyRequest = &modifyRequest{
|
||||
m: &Middleware{withOptions: NewModifyRequest},
|
||||
}
|
||||
|
||||
func NewModifyRequest(optsRaw OptionsRaw) (*Middleware, E.NestedError) {
|
||||
mr := new(modifyRequest)
|
||||
var mrFunc RewriteFunc
|
||||
if common.IsDebug {
|
||||
mrFunc = mr.modifyRequestWithTrace
|
||||
} else {
|
||||
mrFunc = mr.modifyRequest
|
||||
}
|
||||
mr.m = &Middleware{
|
||||
impl: mr,
|
||||
before: Rewrite(mrFunc),
|
||||
}
|
||||
mr.modifyRequestOpts = new(modifyRequestOpts)
|
||||
err := Deserialize(optsRaw, mr.modifyRequestOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mr.m, nil
|
||||
}
|
||||
|
||||
func (mr *modifyRequest) modifyRequest(req *Request) {
|
||||
for k, v := range mr.SetHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
for k, v := range mr.AddHeaders {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
for _, k := range mr.HideHeaders {
|
||||
req.Header.Del(k)
|
||||
}
|
||||
}
|
||||
|
||||
func (mr *modifyRequest) modifyRequestWithTrace(req *Request) {
|
||||
mr.m.AddTraceRequest("before modify request", req)
|
||||
mr.modifyRequest(req)
|
||||
mr.m.AddTraceRequest("after modify request", req)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func TestSetModifyRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("set_options", func(t *testing.T) {
|
||||
mr, err := ModifyRequest.m.WithOptionsClone(opts, nil)
|
||||
mr, err := ModifyRequest.m.WithOptionsClone(opts)
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectDeepEqual(t, mr.impl.(*modifyRequest).SetHeaders, opts["set_headers"].(map[string]string))
|
||||
ExpectDeepEqual(t, mr.impl.(*modifyRequest).AddHeaders, opts["add_headers"].(map[string]string))
|
||||
61
internal/net/http/middleware/modify_response.go
Normal file
61
internal/net/http/middleware/modify_response.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type (
|
||||
modifyResponse struct {
|
||||
*modifyResponseOpts
|
||||
m *Middleware
|
||||
}
|
||||
// order: set_headers -> add_headers -> hide_headers
|
||||
modifyResponseOpts struct {
|
||||
SetHeaders map[string]string
|
||||
AddHeaders map[string]string
|
||||
HideHeaders []string
|
||||
}
|
||||
)
|
||||
|
||||
var ModifyResponse = &modifyResponse{
|
||||
m: &Middleware{withOptions: NewModifyResponse},
|
||||
}
|
||||
|
||||
func NewModifyResponse(optsRaw OptionsRaw) (*Middleware, E.NestedError) {
|
||||
mr := new(modifyResponse)
|
||||
mr.m = &Middleware{impl: mr}
|
||||
if common.IsDebug {
|
||||
mr.m.modifyResponse = mr.modifyResponseWithTrace
|
||||
} else {
|
||||
mr.m.modifyResponse = mr.modifyResponse
|
||||
}
|
||||
mr.modifyResponseOpts = new(modifyResponseOpts)
|
||||
err := Deserialize(optsRaw, mr.modifyResponseOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mr.m, nil
|
||||
}
|
||||
|
||||
func (mr *modifyResponse) modifyResponse(resp *http.Response) error {
|
||||
for k, v := range mr.SetHeaders {
|
||||
resp.Header.Set(k, v)
|
||||
}
|
||||
for k, v := range mr.AddHeaders {
|
||||
resp.Header.Add(k, v)
|
||||
}
|
||||
for _, k := range mr.HideHeaders {
|
||||
resp.Header.Del(k)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mr *modifyResponse) modifyResponseWithTrace(resp *http.Response) error {
|
||||
mr.m.AddTraceResponse("before modify response", resp)
|
||||
err := mr.modifyResponse(resp)
|
||||
mr.m.AddTraceResponse("after modify response", resp)
|
||||
return err
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func TestSetModifyResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("set_options", func(t *testing.T) {
|
||||
mr, err := ModifyResponse.m.WithOptionsClone(opts, nil)
|
||||
mr, err := ModifyResponse.m.WithOptionsClone(opts)
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectDeepEqual(t, mr.impl.(*modifyResponse).SetHeaders, opts["set_headers"].(map[string]string))
|
||||
ExpectDeepEqual(t, mr.impl.(*modifyResponse).AddHeaders, opts["add_headers"].(map[string]string))
|
||||
115
internal/net/http/middleware/real_ip.go
Normal file
115
internal/net/http/middleware/real_ip.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
// https://nginx.org/en/docs/http/ngx_http_realip_module.html
|
||||
|
||||
type realIP struct {
|
||||
*realIPOpts
|
||||
m *Middleware
|
||||
}
|
||||
|
||||
type realIPOpts struct {
|
||||
// Header is the name of the header to use for the real client IP
|
||||
Header string
|
||||
// From is a list of Address / CIDRs to trust
|
||||
From []*types.CIDR
|
||||
/*
|
||||
If recursive search is disabled,
|
||||
the original client address that matches one of the trusted addresses is replaced by
|
||||
the last address sent in the request header field defined by the Header field.
|
||||
If recursive search is enabled,
|
||||
the original client address that matches one of the trusted addresses is replaced by
|
||||
the last non-trusted address sent in the request header field.
|
||||
*/
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
var RealIP = &realIP{
|
||||
m: &Middleware{withOptions: NewRealIP},
|
||||
}
|
||||
|
||||
var realIPOptsDefault = func() *realIPOpts {
|
||||
return &realIPOpts{
|
||||
Header: "X-Real-IP",
|
||||
From: []*types.CIDR{},
|
||||
}
|
||||
}
|
||||
|
||||
func NewRealIP(opts OptionsRaw) (*Middleware, E.NestedError) {
|
||||
riWithOpts := new(realIP)
|
||||
riWithOpts.m = &Middleware{
|
||||
impl: riWithOpts,
|
||||
before: Rewrite(riWithOpts.setRealIP),
|
||||
}
|
||||
riWithOpts.realIPOpts = realIPOptsDefault()
|
||||
err := Deserialize(opts, riWithOpts.realIPOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return riWithOpts.m, nil
|
||||
}
|
||||
|
||||
func (ri *realIP) isInCIDRList(ip net.IP) bool {
|
||||
for _, CIDR := range ri.From {
|
||||
if CIDR.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// not in any CIDR
|
||||
return false
|
||||
}
|
||||
|
||||
func (ri *realIP) setRealIP(req *Request) {
|
||||
clientIPStr, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
clientIPStr = req.RemoteAddr
|
||||
}
|
||||
clientIP := net.ParseIP(clientIPStr)
|
||||
|
||||
var isTrusted = false
|
||||
for _, CIDR := range ri.From {
|
||||
if CIDR.Contains(clientIP) {
|
||||
isTrusted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isTrusted {
|
||||
ri.m.AddTracef("client ip %s is not trusted", clientIP).With("allowed CIDRs", ri.From)
|
||||
return
|
||||
}
|
||||
|
||||
var realIPs = req.Header.Values(ri.Header)
|
||||
var lastNonTrustedIP string
|
||||
|
||||
if len(realIPs) == 0 {
|
||||
ri.m.AddTracef("no real ip found in header %s", ri.Header).WithRequest(req)
|
||||
return
|
||||
}
|
||||
|
||||
if !ri.Recursive {
|
||||
lastNonTrustedIP = realIPs[len(realIPs)-1]
|
||||
} else {
|
||||
for _, r := range realIPs {
|
||||
if !ri.isInCIDRList(net.ParseIP(r)) {
|
||||
lastNonTrustedIP = r
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lastNonTrustedIP == "" {
|
||||
ri.m.AddTracef("no non-trusted ip found").With("allowed CIDRs", ri.From).With("ips", realIPs)
|
||||
return
|
||||
}
|
||||
|
||||
req.RemoteAddr = lastNonTrustedIP
|
||||
req.Header.Set(ri.Header, lastNonTrustedIP)
|
||||
req.Header.Set("X-Real-IP", lastNonTrustedIP)
|
||||
req.Header.Set(xForwardedFor, lastNonTrustedIP)
|
||||
ri.m.AddTracef("set real ip %s", lastNonTrustedIP)
|
||||
}
|
||||
77
internal/net/http/middleware/real_ip_test.go
Normal file
77
internal/net/http/middleware/real_ip_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestSetRealIPOpts(t *testing.T) {
|
||||
opts := OptionsRaw{
|
||||
"header": "X-Real-IP",
|
||||
"from": []string{
|
||||
"127.0.0.0/8",
|
||||
"192.168.0.0/16",
|
||||
"172.16.0.0/12",
|
||||
},
|
||||
"recursive": true,
|
||||
}
|
||||
optExpected := &realIPOpts{
|
||||
Header: "X-Real-IP",
|
||||
From: []*types.CIDR{
|
||||
{
|
||||
IP: net.ParseIP("127.0.0.0"),
|
||||
Mask: net.IPv4Mask(255, 0, 0, 0),
|
||||
},
|
||||
{
|
||||
IP: net.ParseIP("192.168.0.0"),
|
||||
Mask: net.IPv4Mask(255, 255, 0, 0),
|
||||
},
|
||||
{
|
||||
IP: net.ParseIP("172.16.0.0"),
|
||||
Mask: net.IPv4Mask(255, 240, 0, 0),
|
||||
},
|
||||
},
|
||||
Recursive: true,
|
||||
}
|
||||
|
||||
ri, err := NewRealIP(opts)
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectEqual(t, ri.impl.(*realIP).Header, optExpected.Header)
|
||||
ExpectEqual(t, ri.impl.(*realIP).Recursive, optExpected.Recursive)
|
||||
for i, CIDR := range ri.impl.(*realIP).From {
|
||||
ExpectEqual(t, CIDR.String(), optExpected.From[i].String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetRealIP(t *testing.T) {
|
||||
const (
|
||||
testHeader = "X-Real-IP"
|
||||
testRealIP = "192.168.1.1"
|
||||
)
|
||||
opts := OptionsRaw{
|
||||
"header": testHeader,
|
||||
"from": []string{"0.0.0.0/0"},
|
||||
}
|
||||
optsMr := OptionsRaw{
|
||||
"set_headers": map[string]string{testHeader: testRealIP},
|
||||
}
|
||||
realip, err := NewRealIP(opts)
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
mr, err := NewModifyRequest(optsMr)
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
mid := BuildMiddlewareFromChain("test", []*Middleware{mr, realip})
|
||||
|
||||
result, err := newMiddlewareTest(mid, nil)
|
||||
ExpectNoError(t, err.Error())
|
||||
t.Log(traces)
|
||||
ExpectEqual(t, result.ResponseStatus, http.StatusOK)
|
||||
ExpectEqual(t, strings.Split(result.RemoteAddr, ":")[0], testRealIP)
|
||||
ExpectEqual(t, result.RequestHeaders.Get(xForwardedFor), testRealIP)
|
||||
}
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
)
|
||||
|
||||
var RedirectHTTP = &Middleware{
|
||||
before: func(next http.Handler, w ResponseWriter, r *Request) {
|
||||
before: func(next http.HandlerFunc, w ResponseWriter, r *Request) {
|
||||
if r.TLS == nil {
|
||||
r.URL.Scheme = "https"
|
||||
r.URL.Host = r.URL.Hostname() + ":" + common.ProxyHTTPSPort
|
||||
http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
next(w, r)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
deny:
|
||||
- use: ModifyRequest
|
||||
setHeaders:
|
||||
X-Real-IP: 192.168.1.1:1234
|
||||
- use: RealIP
|
||||
header: X-Real-IP
|
||||
from:
|
||||
- 0.0.0.0/0
|
||||
- use: CIDRWhitelist
|
||||
allow:
|
||||
- 192.168.0.0/24
|
||||
accept:
|
||||
- use: ModifyRequest
|
||||
setHeaders:
|
||||
X-Real-IP: 192.168.0.1:1234
|
||||
- use: RealIP
|
||||
header: X-Real-IP
|
||||
from:
|
||||
- 0.0.0.0/0
|
||||
- use: CIDRWhitelist
|
||||
allow:
|
||||
- 192.168.0.0/24
|
||||
@@ -0,0 +1,41 @@
|
||||
theGreatPretender:
|
||||
- use: HideXForwarded
|
||||
- use: ModifyRequest
|
||||
setHeaders:
|
||||
X-Real-IP: 6.6.6.6
|
||||
- use: ModifyResponse
|
||||
hideHeaders:
|
||||
- X-Test3
|
||||
- X-Test4
|
||||
|
||||
notAuthenticAuthentik:
|
||||
- use: RedirectHTTP
|
||||
- use: ForwardAuth
|
||||
address: https://authentik.company
|
||||
trustForwardHeader: true
|
||||
addAuthCookiesToResponse:
|
||||
- session_id
|
||||
- user_id
|
||||
authResponseHeaders:
|
||||
- X-Auth-SessionID
|
||||
- X-Auth-UserID
|
||||
- use: CustomErrorPage
|
||||
|
||||
realIPAuthentik:
|
||||
- use: RedirectHTTP
|
||||
- use: RealIP
|
||||
header: X-Real-IP
|
||||
from:
|
||||
- "127.0.0.0/8"
|
||||
- "192.168.0.0/16"
|
||||
- "172.16.0.0/12"
|
||||
recursive: true
|
||||
- use: ForwardAuth
|
||||
address: https://authentik.company
|
||||
trustForwardHeader: true
|
||||
|
||||
testFakeRealIP:
|
||||
- use: ModifyRequest
|
||||
setHeaders:
|
||||
CF-Connecting-IP: 127.0.0.1
|
||||
- use: CloudflareRealIP
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
gpHTTP "github.com/yusing/go-proxy/internal/http"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
)
|
||||
|
||||
//go:embed test_data/sample_headers.json
|
||||
@@ -20,6 +21,9 @@ var testHeaders http.Header
|
||||
const testHost = "example.com"
|
||||
|
||||
func init() {
|
||||
if !common.IsTest {
|
||||
return
|
||||
}
|
||||
tmp := map[string]string{}
|
||||
err := json.Unmarshal(testHeadersRaw, &tmp)
|
||||
if err != nil {
|
||||
@@ -31,13 +35,15 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
type requestHeaderRecorder struct {
|
||||
type requestRecorder struct {
|
||||
parent http.RoundTripper
|
||||
reqHeaders http.Header
|
||||
headers http.Header
|
||||
remoteAddr string
|
||||
}
|
||||
|
||||
func (rt *requestHeaderRecorder) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
rt.reqHeaders = req.Header
|
||||
func (rt *requestRecorder) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
rt.headers = req.Header
|
||||
rt.remoteAddr = req.RemoteAddr
|
||||
if rt.parent != nil {
|
||||
return rt.parent.RoundTrip(req)
|
||||
}
|
||||
@@ -46,6 +52,7 @@ func (rt *requestHeaderRecorder) RoundTrip(req *http.Request) (*http.Response, e
|
||||
Header: testHeaders,
|
||||
Body: io.NopCloser(bytes.NewBufferString("OK")),
|
||||
Request: req,
|
||||
TLS: req.TLS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -53,6 +60,7 @@ type TestResult struct {
|
||||
RequestHeaders http.Header
|
||||
ResponseHeaders http.Header
|
||||
ResponseStatus int
|
||||
RemoteAddr string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
@@ -65,7 +73,7 @@ type testArgs struct {
|
||||
|
||||
func newMiddlewareTest(middleware *Middleware, args *testArgs) (*TestResult, E.NestedError) {
|
||||
var body io.Reader
|
||||
var rt = new(requestHeaderRecorder)
|
||||
var rr = new(requestRecorder)
|
||||
var proxyURL *url.URL
|
||||
var requestTarget string
|
||||
var err error
|
||||
@@ -98,17 +106,16 @@ func newMiddlewareTest(middleware *Middleware, args *testArgs) (*TestResult, E.N
|
||||
if err != nil {
|
||||
return nil, E.From(err)
|
||||
}
|
||||
rt.parent = http.DefaultTransport
|
||||
rr.parent = http.DefaultTransport
|
||||
} else {
|
||||
proxyURL, _ = url.Parse("https://" + testHost) // dummy url, no actual effect
|
||||
}
|
||||
rp := gpHTTP.NewReverseProxy(proxyURL, rt)
|
||||
setOptErr := PatchReverseProxy(rp, map[string]OptionsRaw{
|
||||
middleware.name: args.middlewareOpt,
|
||||
})
|
||||
rp := gphttp.NewReverseProxy(proxyURL, rr)
|
||||
mid, setOptErr := middleware.WithOptionsClone(args.middlewareOpt)
|
||||
if setOptErr != nil {
|
||||
return nil, setOptErr
|
||||
}
|
||||
patchReverseProxy(middleware.name, rp, []*Middleware{mid})
|
||||
rp.ServeHTTP(w, req)
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
@@ -117,9 +124,10 @@ func newMiddlewareTest(middleware *Middleware, args *testArgs) (*TestResult, E.N
|
||||
return nil, E.From(err)
|
||||
}
|
||||
return &TestResult{
|
||||
RequestHeaders: rt.reqHeaders,
|
||||
RequestHeaders: rr.headers,
|
||||
ResponseHeaders: resp.Header,
|
||||
ResponseStatus: resp.StatusCode,
|
||||
RemoteAddr: rr.remoteAddr,
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
113
internal/net/http/middleware/trace.go
Normal file
113
internal/net/http/middleware/trace.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type Trace struct {
|
||||
Time string `json:"time,omitempty"`
|
||||
Caller string `json:"caller,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Message string `json:"msg"`
|
||||
ReqHeaders map[string]string `json:"req_headers,omitempty"`
|
||||
RespHeaders map[string]string `json:"resp_headers,omitempty"`
|
||||
RespStatus int `json:"resp_status,omitempty"`
|
||||
Additional map[string]any `json:"additional,omitempty"`
|
||||
}
|
||||
|
||||
type Traces []*Trace
|
||||
|
||||
var traces = Traces{}
|
||||
var tracesMu sync.Mutex
|
||||
|
||||
const MaxTraceNum = 100
|
||||
|
||||
func GetAllTrace() []*Trace {
|
||||
return traces
|
||||
}
|
||||
|
||||
func (tr *Trace) WithRequest(req *Request) *Trace {
|
||||
if tr == nil {
|
||||
return nil
|
||||
}
|
||||
tr.URL = req.RequestURI
|
||||
tr.ReqHeaders = gphttp.HeaderToMap(req.Header)
|
||||
return tr
|
||||
}
|
||||
|
||||
func (tr *Trace) WithResponse(resp *Response) *Trace {
|
||||
if tr == nil {
|
||||
return nil
|
||||
}
|
||||
tr.URL = resp.Request.RequestURI
|
||||
tr.ReqHeaders = gphttp.HeaderToMap(resp.Request.Header)
|
||||
tr.RespHeaders = gphttp.HeaderToMap(resp.Header)
|
||||
tr.RespStatus = resp.StatusCode
|
||||
return tr
|
||||
}
|
||||
|
||||
func (tr *Trace) With(what string, additional any) *Trace {
|
||||
if tr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tr.Additional == nil {
|
||||
tr.Additional = map[string]any{}
|
||||
}
|
||||
tr.Additional[what] = additional
|
||||
return tr
|
||||
}
|
||||
|
||||
func (tr *Trace) WithError(err error) *Trace {
|
||||
if tr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tr.Additional == nil {
|
||||
tr.Additional = map[string]any{}
|
||||
}
|
||||
tr.Additional["error"] = err.Error()
|
||||
return tr
|
||||
}
|
||||
|
||||
func (m *Middleware) EnableTrace() {
|
||||
m.trace = true
|
||||
for _, child := range m.children {
|
||||
child.parent = m
|
||||
child.EnableTrace()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Middleware) AddTracef(msg string, args ...any) *Trace {
|
||||
if !m.trace {
|
||||
return nil
|
||||
}
|
||||
return addTrace(&Trace{
|
||||
Time: U.FormatTime(time.Now()),
|
||||
Caller: m.Fullname(),
|
||||
Message: fmt.Sprintf(msg, args...),
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Middleware) AddTraceRequest(msg string, req *Request) *Trace {
|
||||
return m.AddTracef("%s", msg).WithRequest(req)
|
||||
}
|
||||
|
||||
func (m *Middleware) AddTraceResponse(msg string, resp *Response) *Trace {
|
||||
return m.AddTracef("%s", msg).WithResponse(resp)
|
||||
}
|
||||
|
||||
func addTrace(t *Trace) *Trace {
|
||||
tracesMu.Lock()
|
||||
defer tracesMu.Unlock()
|
||||
if len(traces) > MaxTraceNum {
|
||||
traces = traces[1:]
|
||||
}
|
||||
traces = append(traces, t)
|
||||
return t
|
||||
}
|
||||
44
internal/net/http/middleware/x_forwarded.go
Normal file
44
internal/net/http/middleware/x_forwarded.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
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 SetXForwarded = &Middleware{
|
||||
before: Rewrite(func(req *Request) {
|
||||
req.Header.Del("Forwarded")
|
||||
req.Header.Del(xForwardedFor)
|
||||
req.Header.Del(xForwardedHost)
|
||||
req.Header.Del(xForwardedProto)
|
||||
clientIP, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err == nil {
|
||||
req.Header.Set(xForwardedFor, clientIP)
|
||||
} else {
|
||||
req.Header.Set(xForwardedFor, req.RemoteAddr)
|
||||
}
|
||||
req.Header.Set(xForwardedHost, req.Host)
|
||||
if req.TLS == nil {
|
||||
req.Header.Set(xForwardedProto, "http")
|
||||
} else {
|
||||
req.Header.Set(xForwardedProto, "https")
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
var HideXForwarded = &Middleware{
|
||||
before: Rewrite(func(req *Request) {
|
||||
req.Header.Del("Forwarded")
|
||||
req.Header.Del(xForwardedFor)
|
||||
req.Header.Del(xForwardedHost)
|
||||
req.Header.Del(xForwardedProto)
|
||||
}),
|
||||
}
|
||||
@@ -21,9 +21,12 @@ import (
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
// A ProxyRequest contains a request to be rewritten by a [ReverseProxy].
|
||||
@@ -58,39 +61,6 @@ type ProxyRequest struct {
|
||||
// r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
|
||||
// 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"]
|
||||
if len(prior) > 0 {
|
||||
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// ReverseProxy is an HTTP Handler that takes an incoming request and
|
||||
// sends it to another server, proxying the response back to the
|
||||
@@ -99,29 +69,6 @@ func (r *ProxyRequest) AddXForwarded() {
|
||||
// 1xx responses are forwarded to the client if the underlying
|
||||
// transport supports ClientTrace.Got1xxResponse.
|
||||
type ReverseProxy struct {
|
||||
// Rewrite must be a function which modifies
|
||||
// the request into a new request to be sent
|
||||
// using Transport. Its response is then copied
|
||||
// back to the original client unmodified.
|
||||
// Rewrite must not access the provided ProxyRequest
|
||||
// or its contents after returning.
|
||||
//
|
||||
// The Forwarded, X-Forwarded, X-Forwarded-Host,
|
||||
// and X-Forwarded-Proto headers are removed from the
|
||||
// outbound request before Rewrite is called. See also
|
||||
// the ProxyRequest.SetXForwarded method.
|
||||
//
|
||||
// Unparsable query parameters are removed from the
|
||||
// outbound request before Rewrite is called.
|
||||
// The Rewrite function may copy the inbound URL's
|
||||
// RawQuery to the outbound URL to preserve the original
|
||||
// parameter string. Note that this can lead to security
|
||||
// issues if the proxy's interpretation of query parameters
|
||||
// does not match that of the downstream server.
|
||||
//
|
||||
// At most one of Rewrite or Director may be set.
|
||||
Rewrite func(*ProxyRequest)
|
||||
|
||||
// The transport used to perform proxy requests.
|
||||
// If nil, http.DefaultTransport is used.
|
||||
Transport http.RoundTripper
|
||||
@@ -138,13 +85,8 @@ type ReverseProxy struct {
|
||||
ModifyResponse func(*http.Response) error
|
||||
|
||||
ServeHTTP http.HandlerFunc
|
||||
}
|
||||
|
||||
// A BufferPool is an interface for getting and returning temporary
|
||||
// byte slices for use by [io.CopyBuffer].
|
||||
type BufferPool interface {
|
||||
Get() []byte
|
||||
Put([]byte)
|
||||
TargetURL *url.URL
|
||||
}
|
||||
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
@@ -203,11 +145,10 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
|
||||
//
|
||||
|
||||
func NewReverseProxy(target *url.URL, transport http.RoundTripper) *ReverseProxy {
|
||||
rp := &ReverseProxy{
|
||||
Rewrite: func(pr *ProxyRequest) {
|
||||
rewriteRequestURL(pr.Out, target)
|
||||
}, Transport: transport,
|
||||
if transport == nil {
|
||||
panic("nil transport")
|
||||
}
|
||||
rp := &ReverseProxy{Transport: transport, TargetURL: target}
|
||||
rp.ServeHTTP = rp.serveHTTP
|
||||
return rp
|
||||
}
|
||||
@@ -224,6 +165,14 @@ func rewriteRequestURL(req *http.Request, target *url.URL) {
|
||||
}
|
||||
}
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -241,16 +190,14 @@ var hopHeaders = []string{
|
||||
"Upgrade",
|
||||
}
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled),
|
||||
errors.Is(err, io.EOF):
|
||||
logger.Debugf("http proxy to %s error: %s", r.URL.String(), err)
|
||||
default:
|
||||
logger.Errorf("http proxy to %s error: %s", r.URL.String(), err)
|
||||
}
|
||||
if writeHeader {
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
}
|
||||
@@ -316,6 +263,7 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
outreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate
|
||||
}
|
||||
|
||||
rewriteRequestURL(outreq, p.TargetURL)
|
||||
outreq.Close = false
|
||||
|
||||
reqUpType := UpgradeType(outreq.Header)
|
||||
@@ -342,17 +290,27 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
outreq.Header.Set("Upgrade", reqUpType)
|
||||
}
|
||||
|
||||
outreq.Header.Del("Forwarded")
|
||||
// outreq.Header.Del("X-Forwarded-For")
|
||||
// outreq.Header.Del("X-Forwarded-Host")
|
||||
// outreq.Header.Del("X-Forwarded-Proto")
|
||||
|
||||
pr := &ProxyRequest{
|
||||
In: req,
|
||||
Out: outreq,
|
||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
// If we aren't the first proxy retain prior
|
||||
// X-Forwarded-For information as a comma+space
|
||||
// separated list and fold multiple headers into one.
|
||||
prior, ok := outreq.Header["X-Forwarded-For"]
|
||||
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
|
||||
if len(prior) > 0 {
|
||||
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
||||
}
|
||||
if !omit {
|
||||
outreq.Header.Set("X-Forwarded-For", clientIP)
|
||||
}
|
||||
}
|
||||
p.Rewrite(pr)
|
||||
outreq = pr.Out
|
||||
if req.TLS == nil {
|
||||
outreq.Header.Set("X-Forwarded-Proto", "http")
|
||||
outreq.Header.Set("X-Forwarded-Scheme", "http")
|
||||
} else {
|
||||
outreq.Header.Set("X-Forwarded-Proto", "https")
|
||||
outreq.Header.Set("X-Forwarded-Scheme", "https")
|
||||
}
|
||||
outreq.Header.Set("X-Forwarded-Host", req.Host)
|
||||
|
||||
if _, ok := outreq.Header["User-Agent"]; !ok {
|
||||
// If the outbound request doesn't have a User-Agent header set,
|
||||
@@ -360,15 +318,21 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
outreq.Header.Set("User-Agent", "")
|
||||
}
|
||||
|
||||
var (
|
||||
roundTripMutex sync.Mutex
|
||||
roundTripDone bool
|
||||
)
|
||||
trace := &httptrace.ClientTrace{
|
||||
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
|
||||
h := rw.Header()
|
||||
// copyHeader(h, http.Header(header))
|
||||
for k, vv := range header {
|
||||
for _, v := range vv {
|
||||
h.Add(k, v)
|
||||
}
|
||||
roundTripMutex.Lock()
|
||||
defer roundTripMutex.Unlock()
|
||||
if roundTripDone {
|
||||
// If RoundTrip has returned, don't try to further modify
|
||||
// the ResponseWriter's header map.
|
||||
return nil
|
||||
}
|
||||
h := rw.Header()
|
||||
copyHeader(h, http.Header(header))
|
||||
rw.WriteHeader(code)
|
||||
|
||||
// Clear headers, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses
|
||||
@@ -379,6 +343,9 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
outreq = outreq.WithContext(httptrace.WithClientTrace(outreq.Context(), trace))
|
||||
|
||||
res, err := transport.RoundTrip(outreq)
|
||||
roundTripMutex.Lock()
|
||||
roundTripDone = true
|
||||
roundTripMutex.Unlock()
|
||||
if err != nil {
|
||||
p.errorHandler(rw, outreq, err, false)
|
||||
errMsg := err.Error()
|
||||
@@ -389,7 +356,7 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
ProtoMajor: outreq.ProtoMajor,
|
||||
ProtoMinor: outreq.ProtoMinor,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(errMsg))),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte("Origin server is not reachable."))),
|
||||
Request: outreq,
|
||||
ContentLength: int64(len(errMsg)),
|
||||
TLS: outreq.TLS,
|
||||
@@ -405,6 +372,8 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
RemoveHopByHopHeaders(res.Header)
|
||||
|
||||
if !p.modifyResponse(rw, res, outreq) {
|
||||
return
|
||||
}
|
||||
@@ -424,9 +393,11 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
rw.WriteHeader(res.StatusCode)
|
||||
|
||||
_, err = io.Copy(rw, res.Body)
|
||||
err = U.Copy2(req.Context(), rw, res.Body)
|
||||
if err != nil {
|
||||
p.errorHandler(rw, req, err, true)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
p.errorHandler(rw, req, err, true)
|
||||
}
|
||||
res.Body.Close()
|
||||
return
|
||||
}
|
||||
@@ -531,17 +502,9 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R
|
||||
p.errorHandler(rw, req, fmt.Errorf("response flush: %s", err), true)
|
||||
return
|
||||
}
|
||||
errc := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(conn, backConn)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(backConn, conn)
|
||||
errc <- err
|
||||
}()
|
||||
<-errc
|
||||
bdp := U.NewBidirectionalPipe(req.Context(), conn, backConn)
|
||||
bdp.Start()
|
||||
}
|
||||
|
||||
func IsPrint(s string) bool {
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
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"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -28,6 +28,7 @@ type (
|
||||
StopSignal T.Signal
|
||||
DockerHost string
|
||||
ContainerName string
|
||||
ContainerID string
|
||||
ContainerRunning bool
|
||||
}
|
||||
StreamEntry struct {
|
||||
@@ -42,10 +43,12 @@ func (rp *ReverseProxyEntry) UseIdleWatcher() bool {
|
||||
return rp.IdleTimeout > 0 && rp.DockerHost != ""
|
||||
}
|
||||
|
||||
func ValidateEntry(m *M.RawEntry) (any, E.NestedError) {
|
||||
if !m.FillMissingFields() {
|
||||
return nil, E.Missing("fields")
|
||||
}
|
||||
func (rp *ReverseProxyEntry) IsDocker() bool {
|
||||
return rp.DockerHost != ""
|
||||
}
|
||||
|
||||
func ValidateEntry(m *types.RawEntry) (any, E.NestedError) {
|
||||
m.FillMissingFields()
|
||||
|
||||
scheme, err := T.NewScheme(m.Scheme)
|
||||
if err.HasError() {
|
||||
@@ -65,7 +68,7 @@ func ValidateEntry(m *M.RawEntry) (any, E.NestedError) {
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func validateRPEntry(m *M.RawEntry, s T.Scheme, b E.Builder) *ReverseProxyEntry {
|
||||
func validateRPEntry(m *types.RawEntry, s T.Scheme, b E.Builder) *ReverseProxyEntry {
|
||||
var stopTimeOut time.Duration
|
||||
|
||||
host, err := T.ValidateHost(m.Host)
|
||||
@@ -115,11 +118,12 @@ func validateRPEntry(m *M.RawEntry, s T.Scheme, b E.Builder) *ReverseProxyEntry
|
||||
StopSignal: stopSignal,
|
||||
DockerHost: m.DockerHost,
|
||||
ContainerName: m.ContainerName,
|
||||
ContainerID: m.ContainerID,
|
||||
ContainerRunning: m.Running,
|
||||
}
|
||||
}
|
||||
|
||||
func validateStreamEntry(m *M.RawEntry, b E.Builder) *StreamEntry {
|
||||
func validateStreamEntry(m *types.RawEntry, b E.Builder) *StreamEntry {
|
||||
host, err := T.ValidateHost(m.Host)
|
||||
b.Add(err)
|
||||
|
||||
|
||||
@@ -33,9 +33,8 @@ func (p Port) String() string {
|
||||
}
|
||||
|
||||
const (
|
||||
MinPort = 0
|
||||
MaxPort = 65535
|
||||
ErrPort = Port(-1)
|
||||
NoPort = Port(-1)
|
||||
ZeroPort = Port(0)
|
||||
MinPort = 0
|
||||
MaxPort = 65535
|
||||
ErrPort = Port(-1)
|
||||
NoPort = Port(0)
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ type StreamPort struct {
|
||||
ProxyPort Port `json:"proxy"`
|
||||
}
|
||||
|
||||
func ValidateStreamPort(p string) (StreamPort, E.NestedError) {
|
||||
func ValidateStreamPort(p string) (_ StreamPort, err E.NestedError) {
|
||||
split := strings.Split(p, ":")
|
||||
|
||||
switch len(split) {
|
||||
@@ -21,24 +21,26 @@ func ValidateStreamPort(p string) (StreamPort, E.NestedError) {
|
||||
case 2:
|
||||
break
|
||||
default:
|
||||
return ErrStreamPort, E.Invalid("stream port", p).With("too many colons")
|
||||
err = E.Invalid("stream port", p).With("too many colons")
|
||||
return
|
||||
}
|
||||
|
||||
listeningPort, err := ValidatePort(split[0])
|
||||
if err != nil {
|
||||
return ErrStreamPort, err.Subject("listening port")
|
||||
err = err.Subject("listening port")
|
||||
return
|
||||
}
|
||||
|
||||
proxyPort, err := ValidatePort(split[1])
|
||||
|
||||
if err.Is(E.ErrOutOfRange) {
|
||||
return ErrStreamPort, err.Subject("proxy port")
|
||||
} else if proxyPort == 0 {
|
||||
return ErrStreamPort, E.Invalid("proxy port", p)
|
||||
err = err.Subject("proxy port")
|
||||
return
|
||||
} else if err != nil {
|
||||
proxyPort, err = parseNameToPort(split[1])
|
||||
if err != nil {
|
||||
return ErrStreamPort, E.Invalid("proxy port", proxyPort)
|
||||
err = E.Invalid("proxy port", proxyPort)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,5 +54,3 @@ func parseNameToPort(name string) (Port, E.NestedError) {
|
||||
}
|
||||
return Port(port), nil
|
||||
}
|
||||
|
||||
var ErrStreamPort = StreamPort{ErrPort, ErrPort}
|
||||
|
||||
@@ -9,29 +9,31 @@ import (
|
||||
"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"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
)
|
||||
|
||||
type DockerProvider struct {
|
||||
dockerHost, hostname string
|
||||
ExplicitOnly bool
|
||||
name, dockerHost, hostname string
|
||||
ExplicitOnly bool
|
||||
}
|
||||
|
||||
var AliasRefRegex = regexp.MustCompile(`#\d+`)
|
||||
var AliasRefRegexOld = regexp.MustCompile(`\$\d+`)
|
||||
|
||||
func DockerProviderImpl(dockerHost string, explicitOnly bool) (ProviderImpl, E.NestedError) {
|
||||
func DockerProviderImpl(name, dockerHost string, explicitOnly bool) (ProviderImpl, E.NestedError) {
|
||||
hostname, err := D.ParseDockerHostname(dockerHost)
|
||||
if err.HasError() {
|
||||
return nil, err
|
||||
}
|
||||
return &DockerProvider{dockerHost, hostname, explicitOnly}, nil
|
||||
return &DockerProvider{name, dockerHost, hostname, explicitOnly}, nil
|
||||
}
|
||||
|
||||
func (p *DockerProvider) String() string {
|
||||
return fmt.Sprintf("docker:%s", p.dockerHost)
|
||||
return fmt.Sprintf("docker: %s", p.name)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) NewWatcher() W.Watcher {
|
||||
@@ -40,7 +42,7 @@ func (p *DockerProvider) NewWatcher() W.Watcher {
|
||||
|
||||
func (p *DockerProvider) LoadRoutesImpl() (routes R.Routes, err E.NestedError) {
|
||||
routes = R.NewRoutes()
|
||||
entries := M.NewProxyEntries()
|
||||
entries := types.NewProxyEntries()
|
||||
|
||||
info, err := D.GetClientInfo(p.dockerHost, true)
|
||||
if err.HasError() {
|
||||
@@ -63,12 +65,12 @@ func (p *DockerProvider) LoadRoutesImpl() (routes R.Routes, err E.NestedError) {
|
||||
// there may be some valid entries in `en`
|
||||
dups := entries.MergeFrom(newEntries)
|
||||
// add the duplicate proxy entries to the error
|
||||
dups.RangeAll(func(k string, v *M.RawEntry) {
|
||||
dups.RangeAll(func(k string, v *types.RawEntry) {
|
||||
errors.Addf("duplicate alias %s", k)
|
||||
})
|
||||
}
|
||||
|
||||
entries.RangeAll(func(_ string, e *M.RawEntry) {
|
||||
entries.RangeAll(func(_ string, e *types.RawEntry) {
|
||||
e.DockerHost = p.dockerHost
|
||||
})
|
||||
|
||||
@@ -78,18 +80,61 @@ func (p *DockerProvider) LoadRoutesImpl() (routes R.Routes, err E.NestedError) {
|
||||
return routes, errors.Build()
|
||||
}
|
||||
|
||||
func (p *DockerProvider) shouldIgnore(container D.Container) bool {
|
||||
return container.IsExcluded ||
|
||||
!container.IsExplicit && p.ExplicitOnly ||
|
||||
!container.IsExplicit && container.IsDatabase ||
|
||||
strings.HasSuffix(container.ContainerName, "-old")
|
||||
}
|
||||
|
||||
func (p *DockerProvider) OnEvent(event W.Event, routes R.Routes) (res EventResult) {
|
||||
switch event.Action {
|
||||
case events.ActionContainerStart, events.ActionContainerStop:
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
b := E.NewBuilder("event %s error", event)
|
||||
defer b.To(&res.err)
|
||||
|
||||
routes.RangeAll(func(k string, v R.Route) {
|
||||
if v.Entry().ContainerName == event.ActorName {
|
||||
if v.Entry().ContainerID == event.ActorID ||
|
||||
v.Entry().ContainerName == event.ActorName {
|
||||
b.Add(v.Stop())
|
||||
routes.Delete(k)
|
||||
res.nRemoved++
|
||||
}
|
||||
})
|
||||
|
||||
if res.nRemoved == 0 { // id & container name changed
|
||||
// load all routes (rescan)
|
||||
routesNew, err := p.LoadRoutesImpl()
|
||||
routesOld := routes
|
||||
if routesNew.Size() == 0 {
|
||||
b.Add(E.FailWith("rescan routes", err))
|
||||
return
|
||||
}
|
||||
routesNew.Range(func(k string, v R.Route) bool {
|
||||
if !routesOld.Has(k) {
|
||||
routesOld.Store(k, v)
|
||||
b.Add(v.Start())
|
||||
res.nAdded++
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
routesOld.Range(func(k string, v R.Route) bool {
|
||||
if !routesNew.Has(k) {
|
||||
b.Add(v.Stop())
|
||||
routesOld.Delete(k)
|
||||
res.nRemoved++
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
client, err := D.ConnectClient(p.dockerHost)
|
||||
if err.HasError() {
|
||||
b.Add(E.FailWith("connect to docker", err))
|
||||
@@ -101,10 +146,15 @@ func (p *DockerProvider) OnEvent(event W.Event, routes R.Routes) (res EventResul
|
||||
b.Add(E.FailWith("inspect container", err))
|
||||
return
|
||||
}
|
||||
|
||||
if p.shouldIgnore(cont) {
|
||||
return
|
||||
}
|
||||
|
||||
entries, err := p.entriesFromContainerLabels(cont)
|
||||
b.Add(err)
|
||||
|
||||
entries.RangeAll(func(alias string, entry *M.RawEntry) {
|
||||
entries.RangeAll(func(alias string, entry *types.RawEntry) {
|
||||
if routes.Has(alias) {
|
||||
b.Add(E.Duplicated("alias", alias))
|
||||
} else {
|
||||
@@ -123,17 +173,16 @@ 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) (entries M.RawEntries, _ E.NestedError) {
|
||||
entries = M.NewProxyEntries()
|
||||
func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (entries types.RawEntries, _ E.NestedError) {
|
||||
entries = types.NewProxyEntries()
|
||||
|
||||
if container.IsExcluded ||
|
||||
!container.IsExplicit && p.ExplicitOnly {
|
||||
if p.shouldIgnore(container) {
|
||||
return
|
||||
}
|
||||
|
||||
// init entries map for all aliases
|
||||
for _, a := range container.Aliases {
|
||||
entries.Store(a, &M.RawEntry{
|
||||
entries.Store(a, &types.RawEntry{
|
||||
Alias: a,
|
||||
Host: p.hostname,
|
||||
ProxyProperties: container.ProxyProperties,
|
||||
@@ -146,14 +195,14 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (entr
|
||||
}
|
||||
|
||||
// remove all entries that failed to fill in missing fields
|
||||
entries.RemoveAll(func(re *M.RawEntry) bool {
|
||||
return !re.FillMissingFields()
|
||||
entries.RangeAll(func(_ string, re *types.RawEntry) {
|
||||
re.FillMissingFields()
|
||||
})
|
||||
|
||||
return entries, errors.Build().Subject(container.ContainerName)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) applyLabel(container D.Container, entries M.RawEntries, key, val string) (res E.NestedError) {
|
||||
func (p *DockerProvider) applyLabel(container D.Container, entries types.RawEntries, key, val string) (res E.NestedError) {
|
||||
b := E.NewBuilder("errors in label %s", key)
|
||||
defer b.To(&res)
|
||||
|
||||
@@ -180,7 +229,7 @@ func (p *DockerProvider) applyLabel(container D.Container, entries M.RawEntries,
|
||||
}
|
||||
if lbl.Target == D.WildcardAlias {
|
||||
// apply label for all aliases
|
||||
entries.RangeAll(func(a string, e *M.RawEntry) {
|
||||
entries.RangeAll(func(a string, e *types.RawEntry) {
|
||||
if err = D.ApplyLabel(e, lbl); err.HasError() {
|
||||
b.Add(err.Subjectf("alias %s", lbl.Target))
|
||||
}
|
||||
|
||||
@@ -235,73 +235,75 @@ func TestExplicitExclude(t *testing.T) {
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestImplicitExclude(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
},
|
||||
State: "running",
|
||||
}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
//! Now nothing will be implicit excluded
|
||||
//
|
||||
// func TestImplicitExclude(t *testing.T) {
|
||||
// var p DockerProvider
|
||||
// entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
// Names: dummyNames,
|
||||
// Labels: map[string]string{
|
||||
// D.LabelAliases: "a",
|
||||
// "proxy.a.no_tls_verify": "true",
|
||||
// },
|
||||
// State: "running",
|
||||
// }, ""))
|
||||
// ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
// _, ok := entries.Load("a")
|
||||
// ExpectFalse(t, ok)
|
||||
// }
|
||||
|
||||
func TestImplicitExcludeNoExposedPort(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Image: "redis",
|
||||
Names: []string{"redis"},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
},
|
||||
State: "running",
|
||||
}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
// func TestImplicitExcludeNoExposedPort(t *testing.T) {
|
||||
// var p DockerProvider
|
||||
// entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
// Image: "redis",
|
||||
// Names: []string{"redis"},
|
||||
// Ports: []types.Port{
|
||||
// {Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
// },
|
||||
// State: "running",
|
||||
// }, ""))
|
||||
// ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("redis")
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
// _, ok := entries.Load("redis")
|
||||
// ExpectFalse(t, ok)
|
||||
// }
|
||||
|
||||
func TestNotExcludeSpecifiedPort(t *testing.T) {
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Image: "redis",
|
||||
Names: []string{"redis"},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"proxy.redis.port": "6379:6379", // but specified in label
|
||||
},
|
||||
}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
// func TestNotExcludeSpecifiedPort(t *testing.T) {
|
||||
// var p DockerProvider
|
||||
// entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
// Image: "redis",
|
||||
// Names: []string{"redis"},
|
||||
// Ports: []types.Port{
|
||||
// {Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
// },
|
||||
// Labels: map[string]string{
|
||||
// "proxy.redis.port": "6379:6379", // but specified in label
|
||||
// },
|
||||
// }, ""))
|
||||
// ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("redis")
|
||||
ExpectTrue(t, ok)
|
||||
}
|
||||
// _, ok := entries.Load("redis")
|
||||
// ExpectTrue(t, ok)
|
||||
// }
|
||||
|
||||
func TestNotExcludeNonExposedPortHostNetwork(t *testing.T) {
|
||||
var p DockerProvider
|
||||
cont := &types.Container{
|
||||
Image: "redis",
|
||||
Names: []string{"redis"},
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"proxy.redis.port": "6379:6379",
|
||||
},
|
||||
}
|
||||
cont.HostConfig.NetworkMode = "host"
|
||||
// func TestNotExcludeNonExposedPortHostNetwork(t *testing.T) {
|
||||
// var p DockerProvider
|
||||
// cont := &types.Container{
|
||||
// Image: "redis",
|
||||
// Names: []string{"redis"},
|
||||
// Ports: []types.Port{
|
||||
// {Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
// },
|
||||
// Labels: map[string]string{
|
||||
// "proxy.redis.port": "6379:6379",
|
||||
// },
|
||||
// }
|
||||
// cont.HostConfig.NetworkMode = "host"
|
||||
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(cont, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
// entries, err := p.entriesFromContainerLabels(D.FromDocker(cont, ""))
|
||||
// ExpectNoError(t, err.Error())
|
||||
|
||||
_, ok := entries.Load("redis")
|
||||
ExpectTrue(t, ok)
|
||||
}
|
||||
// _, ok := entries.Load("redis")
|
||||
// ExpectTrue(t, ok)
|
||||
// }
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
"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"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
)
|
||||
@@ -71,7 +71,7 @@ func (p *FileProvider) LoadRoutesImpl() (routes R.Routes, res E.NestedError) {
|
||||
b := E.NewBuilder("file %q validation failure", p.fileName)
|
||||
defer b.To(&res)
|
||||
|
||||
entries := M.NewProxyEntries()
|
||||
entries := types.NewProxyEntries()
|
||||
|
||||
data, err := E.Check(os.ReadFile(p.path))
|
||||
if err.HasError() {
|
||||
@@ -79,12 +79,6 @@ func (p *FileProvider) LoadRoutesImpl() (routes R.Routes, res E.NestedError) {
|
||||
return
|
||||
}
|
||||
|
||||
if !common.NoSchemaValidation {
|
||||
if err = Validate(data); err.HasError() {
|
||||
b.Add(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = entries.UnmarshalFromYAML(data); err.HasError() {
|
||||
b.Add(err)
|
||||
return
|
||||
|
||||
@@ -31,8 +31,13 @@ type (
|
||||
OnEvent(event W.Event, routes R.Routes) EventResult
|
||||
String() string
|
||||
}
|
||||
ProviderType string
|
||||
EventResult struct {
|
||||
ProviderType string
|
||||
ProviderStats struct {
|
||||
NumRPs int `json:"num_reverse_proxies"`
|
||||
NumStreams int `json:"num_streams"`
|
||||
Type ProviderType `json:"type"`
|
||||
}
|
||||
EventResult struct {
|
||||
nRemoved int
|
||||
nAdded int
|
||||
err E.NestedError
|
||||
@@ -72,14 +77,9 @@ func NewDockerProvider(name string, dockerHost string) (p *Provider, err E.Neste
|
||||
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, explicitOnly)
|
||||
p.ProviderImpl, err = DockerProviderImpl(name, dockerHost, p.IsExplicitOnly())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -87,6 +87,10 @@ func NewDockerProvider(name string, dockerHost string) (p *Provider, err E.Neste
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Provider) IsExplicitOnly() bool {
|
||||
return p.name[len(p.name)-1] == '!'
|
||||
}
|
||||
|
||||
func (p *Provider) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
@@ -164,6 +168,27 @@ func (p *Provider) LoadRoutes() E.NestedError {
|
||||
return E.FailWith("loading routes", err)
|
||||
}
|
||||
|
||||
func (p *Provider) Statistics() ProviderStats {
|
||||
numRPs := 0
|
||||
numStreams := 0
|
||||
p.routes.RangeAll(func(_ string, r R.Route) {
|
||||
if !r.Started() {
|
||||
return
|
||||
}
|
||||
switch r.Type() {
|
||||
case R.RouteTypeReverseProxy:
|
||||
numRPs++
|
||||
case R.RouteTypeStream:
|
||||
numStreams++
|
||||
}
|
||||
})
|
||||
return ProviderStats{
|
||||
NumRPs: numRPs,
|
||||
NumStreams: numStreams,
|
||||
Type: p.t,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) watchEvents() {
|
||||
p.watcherCtx, p.watcherCancel = context.WithCancel(context.Background())
|
||||
events, errs := p.watcher.Events(p.watcherCtx)
|
||||
@@ -176,7 +201,9 @@ func (p *Provider) watchEvents() {
|
||||
case event := <-events:
|
||||
res := p.OnEvent(event, p.routes)
|
||||
l.Infof("%s event %q", event.Type, event)
|
||||
l.Infof("%d route added, %d routes removed", res.nAdded, res.nRemoved)
|
||||
if res.nAdded > 0 || res.nRemoved > 0 {
|
||||
l.Infof("%d route added, %d routes removed", res.nAdded, res.nRemoved)
|
||||
}
|
||||
if res.err.HasError() {
|
||||
l.Error(res.err)
|
||||
}
|
||||
|
||||
@@ -10,13 +10,12 @@ import (
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"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"
|
||||
. "github.com/yusing/go-proxy/internal/net/http"
|
||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -27,25 +26,30 @@ type (
|
||||
PathPatterns PT.PathPatterns `json:"path_patterns"`
|
||||
|
||||
entry *P.ReverseProxyEntry
|
||||
mux *http.ServeMux
|
||||
handler *ReverseProxy
|
||||
|
||||
regIdleWatcher func() E.NestedError
|
||||
unregIdleWatcher func()
|
||||
handler http.Handler
|
||||
rp *ReverseProxy
|
||||
}
|
||||
|
||||
URL url.URL
|
||||
SubdomainKey = PT.Alias
|
||||
|
||||
ReverseProxyHandler struct {
|
||||
*ReverseProxy
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
findMuxFunc = findMuxAnyDomain
|
||||
|
||||
httpRoutes = F.NewMapOf[SubdomainKey, *HTTPRoute]()
|
||||
httpRoutes = F.NewMapOf[string, *HTTPRoute]()
|
||||
httpRoutesMu sync.Mutex
|
||||
globalMux = http.NewServeMux() // TODO: support regex subdomain matching
|
||||
)
|
||||
|
||||
func (rp ReverseProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
rp.ReverseProxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func SetFindMuxDomains(domains []string) {
|
||||
if len(domains) == 0 {
|
||||
findMuxFunc = findMuxAnyDomain
|
||||
@@ -56,60 +60,31 @@ func SetFindMuxDomains(domains []string) {
|
||||
|
||||
func NewHTTPRoute(entry *P.ReverseProxyEntry) (*HTTPRoute, E.NestedError) {
|
||||
var trans *http.Transport
|
||||
var regIdleWatcher func() E.NestedError
|
||||
var unregIdleWatcher func()
|
||||
|
||||
if entry.NoTLSVerify {
|
||||
trans = common.DefaultTransportNoTLS.Clone()
|
||||
trans = DefaultTransportNoTLS.Clone()
|
||||
} else {
|
||||
trans = common.DefaultTransport.Clone()
|
||||
trans = DefaultTransport.Clone()
|
||||
}
|
||||
|
||||
rp := NewReverseProxy(entry.URL, trans)
|
||||
|
||||
if len(entry.Middlewares) > 0 {
|
||||
err := middleware.PatchReverseProxy(rp, entry.Middlewares)
|
||||
err := middleware.PatchReverseProxy(string(entry.Alias), rp, entry.Middlewares)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if entry.UseIdleWatcher() {
|
||||
// allow time for response header up to `WakeTimeout`
|
||||
if entry.WakeTimeout > trans.ResponseHeaderTimeout {
|
||||
trans.ResponseHeaderTimeout = entry.WakeTimeout
|
||||
}
|
||||
regIdleWatcher = func() E.NestedError {
|
||||
watcher, err := idlewatcher.Register(entry)
|
||||
if err.HasError() {
|
||||
return err
|
||||
}
|
||||
// patch round-tripper
|
||||
rp.Transport = watcher.PatchRoundTripper(trans)
|
||||
return nil
|
||||
}
|
||||
unregIdleWatcher = func() {
|
||||
idlewatcher.Unregister(entry.ContainerName)
|
||||
rp.Transport = trans
|
||||
}
|
||||
}
|
||||
|
||||
httpRoutesMu.Lock()
|
||||
defer httpRoutesMu.Unlock()
|
||||
|
||||
_, exists := httpRoutes.Load(entry.Alias)
|
||||
if exists {
|
||||
return nil, E.Duplicated("HTTPRoute alias", entry.Alias)
|
||||
}
|
||||
|
||||
r := &HTTPRoute{
|
||||
Alias: entry.Alias,
|
||||
TargetURL: (*URL)(entry.URL),
|
||||
PathPatterns: entry.PathPatterns,
|
||||
entry: entry,
|
||||
handler: rp,
|
||||
regIdleWatcher: regIdleWatcher,
|
||||
unregIdleWatcher: unregIdleWatcher,
|
||||
Alias: entry.Alias,
|
||||
TargetURL: (*URL)(entry.URL),
|
||||
PathPatterns: entry.PathPatterns,
|
||||
entry: entry,
|
||||
rp: rp,
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
@@ -119,45 +94,55 @@ func (r *HTTPRoute) String() string {
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) Start() E.NestedError {
|
||||
if r.mux != nil {
|
||||
if r.handler != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
httpRoutesMu.Lock()
|
||||
defer httpRoutesMu.Unlock()
|
||||
|
||||
if r.regIdleWatcher != nil {
|
||||
if err := r.regIdleWatcher(); err.HasError() {
|
||||
r.unregIdleWatcher = nil
|
||||
if r.entry.UseIdleWatcher() {
|
||||
watcher, err := idlewatcher.Register(r.entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.handler = idlewatcher.NewWaker(watcher, r.rp)
|
||||
} else if r.entry.URL.Port() == "0" ||
|
||||
r.entry.IsDocker() && !r.entry.ContainerRunning {
|
||||
return nil
|
||||
} else if len(r.PathPatterns) == 1 && r.PathPatterns[0] == "/" {
|
||||
r.handler = ReverseProxyHandler{r.rp}
|
||||
} else {
|
||||
mux := http.NewServeMux()
|
||||
for _, p := range r.PathPatterns {
|
||||
mux.HandleFunc(string(p), r.rp.ServeHTTP)
|
||||
}
|
||||
r.handler = mux
|
||||
}
|
||||
|
||||
r.mux = http.NewServeMux()
|
||||
for _, p := range r.PathPatterns {
|
||||
r.mux.HandleFunc(string(p), r.handler.ServeHTTP)
|
||||
}
|
||||
|
||||
httpRoutes.Store(r.Alias, r)
|
||||
httpRoutes.Store(string(r.Alias), r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) Stop() E.NestedError {
|
||||
if r.mux == nil {
|
||||
return nil
|
||||
func (r *HTTPRoute) Stop() (_ E.NestedError) {
|
||||
if r.handler == nil {
|
||||
return
|
||||
}
|
||||
|
||||
httpRoutesMu.Lock()
|
||||
defer httpRoutesMu.Unlock()
|
||||
|
||||
if r.unregIdleWatcher != nil {
|
||||
r.unregIdleWatcher()
|
||||
r.unregIdleWatcher = nil
|
||||
if waker, ok := r.handler.(*idlewatcher.Waker); ok {
|
||||
waker.Unregister()
|
||||
}
|
||||
|
||||
r.mux = nil
|
||||
httpRoutes.Delete(r.Alias)
|
||||
return nil
|
||||
r.handler = nil
|
||||
httpRoutes.Delete(string(r.Alias))
|
||||
return
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) Started() bool {
|
||||
return r.handler != nil
|
||||
}
|
||||
|
||||
func (u *URL) String() string {
|
||||
@@ -189,21 +174,21 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func findMuxAnyDomain(host string) (*http.ServeMux, error) {
|
||||
func findMuxAnyDomain(host string) (http.Handler, 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
|
||||
if r, ok := httpRoutes.Load(sd); ok {
|
||||
return r.handler, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no such route: %s", sd)
|
||||
}
|
||||
|
||||
func findMuxByDomains(domains []string) func(host string) (*http.ServeMux, error) {
|
||||
return func(host string) (*http.ServeMux, error) {
|
||||
func findMuxByDomains(domains []string) func(host string) (http.Handler, error) {
|
||||
return func(host string) (http.Handler, error) {
|
||||
var subdomain string
|
||||
|
||||
for _, domain := range domains {
|
||||
@@ -218,8 +203,8 @@ func findMuxByDomains(domains []string) func(host string) (*http.ServeMux, error
|
||||
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
|
||||
if r, ok := httpRoutes.Load(subdomain); ok {
|
||||
return r.handler, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no such route: %s", subdomain)
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
modifyRequest struct {
|
||||
*modifyRequestOpts
|
||||
m *Middleware
|
||||
}
|
||||
// order: set_headers -> add_headers -> hide_headers
|
||||
modifyRequestOpts struct {
|
||||
SetHeaders map[string]string
|
||||
AddHeaders map[string]string
|
||||
HideHeaders []string
|
||||
}
|
||||
)
|
||||
|
||||
var ModifyRequest = newModifyRequest()
|
||||
|
||||
func newModifyRequest() (mr *modifyRequest) {
|
||||
mr = new(modifyRequest)
|
||||
mr.m = new(Middleware)
|
||||
mr.m.labelParserMap = D.ValueParserMap{
|
||||
"set_headers": D.YamlLikeMappingParser(true),
|
||||
"add_headers": D.YamlLikeMappingParser(true),
|
||||
"hide_headers": D.YamlStringListParser,
|
||||
}
|
||||
mr.m.withOptions = func(optsRaw OptionsRaw, rp *ReverseProxy) (*Middleware, E.NestedError) {
|
||||
mrWithOpts := new(modifyRequest)
|
||||
mrWithOpts.m = &Middleware{
|
||||
impl: mrWithOpts,
|
||||
rewrite: mrWithOpts.modifyRequest,
|
||||
}
|
||||
mrWithOpts.modifyRequestOpts = new(modifyRequestOpts)
|
||||
err := U.Deserialize(optsRaw, mrWithOpts.modifyRequestOpts)
|
||||
if err != nil {
|
||||
return nil, E.FailWith("set options", err)
|
||||
}
|
||||
return mrWithOpts.m, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (mr *modifyRequest) modifyRequest(req *ProxyRequest) {
|
||||
for k, v := range mr.SetHeaders {
|
||||
req.Out.Header.Set(k, v)
|
||||
}
|
||||
for k, v := range mr.AddHeaders {
|
||||
req.Out.Header.Add(k, v)
|
||||
}
|
||||
for _, k := range mr.HideHeaders {
|
||||
req.Out.Header.Del(k)
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
modifyResponse struct {
|
||||
*modifyResponseOpts
|
||||
m *Middleware
|
||||
}
|
||||
// order: set_headers -> add_headers -> hide_headers
|
||||
modifyResponseOpts struct {
|
||||
SetHeaders map[string]string
|
||||
AddHeaders map[string]string
|
||||
HideHeaders []string
|
||||
}
|
||||
)
|
||||
|
||||
var ModifyResponse = newModifyResponse()
|
||||
|
||||
func newModifyResponse() (mr *modifyResponse) {
|
||||
mr = new(modifyResponse)
|
||||
mr.m = new(Middleware)
|
||||
mr.m.labelParserMap = D.ValueParserMap{
|
||||
"set_headers": D.YamlLikeMappingParser(true),
|
||||
"add_headers": D.YamlLikeMappingParser(true),
|
||||
"hide_headers": D.YamlStringListParser,
|
||||
}
|
||||
mr.m.withOptions = func(optsRaw OptionsRaw, rp *ReverseProxy) (*Middleware, E.NestedError) {
|
||||
mrWithOpts := new(modifyResponse)
|
||||
mrWithOpts.m = &Middleware{
|
||||
impl: mrWithOpts,
|
||||
modifyResponse: mrWithOpts.modifyResponse,
|
||||
}
|
||||
mrWithOpts.modifyResponseOpts = new(modifyResponseOpts)
|
||||
err := U.Deserialize(optsRaw, mrWithOpts.modifyResponseOpts)
|
||||
if err != nil {
|
||||
return nil, E.FailWith("set options", err)
|
||||
}
|
||||
return mrWithOpts.m, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (mr *modifyResponse) modifyResponse(resp *http.Response) error {
|
||||
for k, v := range mr.SetHeaders {
|
||||
resp.Header.Set(k, v)
|
||||
}
|
||||
for k, v := range mr.AddHeaders {
|
||||
resp.Header.Add(k, v)
|
||||
}
|
||||
for _, k := range mr.HideHeaders {
|
||||
resp.Header.Del(k)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user