mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-20 15:31:24 +02:00
Compare commits
69 Commits
v0.10.2
...
v0.11.6-bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c49016f22c | ||
|
|
8da63daf02 | ||
|
|
c5fd21552e | ||
|
|
27409abc24 | ||
|
|
21c9e46274 | ||
|
|
22a12d3116 | ||
|
|
89d93dd878 | ||
|
|
66853dfc52 | ||
|
|
c72f66d64b | ||
|
|
59bc342a40 | ||
|
|
e11579df10 | ||
|
|
6a8f6fb4b5 | ||
|
|
8f20bd3840 | ||
|
|
f1abb745fe | ||
|
|
cb2990f6e8 | ||
|
|
fb2f850311 | ||
|
|
2b9c0f09ee | ||
|
|
efe3eb4ce7 | ||
|
|
a1c1a79976 | ||
|
|
90ba355d16 | ||
|
|
01179adfa8 | ||
|
|
e4be403bef | ||
|
|
e1cdf4da0f | ||
|
|
5148cb3b8b | ||
|
|
56c6a9f8fe | ||
|
|
be257b0532 | ||
|
|
0534bc38b2 | ||
|
|
604e2481a6 | ||
|
|
4f557043a5 | ||
|
|
03d609e4e1 | ||
|
|
db6fc65876 | ||
|
|
c6a05f7b35 | ||
|
|
9e4aa32120 | ||
|
|
759995972d | ||
|
|
03401488f6 | ||
|
|
1e790be70c | ||
|
|
4410637f8b | ||
|
|
3947152336 | ||
|
|
af8d2c74f6 | ||
|
|
e107f8d476 | ||
|
|
b427ff1f88 | ||
|
|
e513db62b0 | ||
|
|
2f33ee02d9 | ||
|
|
59490dcac0 | ||
|
|
5afa93a8f1 | ||
|
|
c8e9ed8440 | ||
|
|
8363dfe257 | ||
|
|
080bbc18eb | ||
|
|
1a0edc8bfe | ||
|
|
e8d1d524b9 | ||
|
|
edada22ac0 | ||
|
|
76fb0cfdbb | ||
|
|
5df2553774 | ||
|
|
31812430f1 | ||
|
|
d668b03175 | ||
|
|
663a107c06 | ||
|
|
806184e98b | ||
|
|
08ee82d7b0 | ||
|
|
bcc19167d4 | ||
|
|
858f65ee5a | ||
|
|
43566bbcfd | ||
|
|
ec8cca1245 | ||
|
|
4a65de99a8 | ||
|
|
7461344004 | ||
|
|
b815c6fd69 | ||
|
|
28c9a2e9d0 | ||
|
|
9e0bdd964c | ||
|
|
077641beaa | ||
|
|
ef483403da |
29
.env.example
29
.env.example
@@ -1,24 +1,29 @@
|
|||||||
|
# docker image tag (latest, nightly)
|
||||||
|
TAG=latest
|
||||||
|
|
||||||
# set timezone to get correct log timestamp
|
# set timezone to get correct log timestamp
|
||||||
TZ=ETC/UTC
|
TZ=ETC/UTC
|
||||||
|
|
||||||
|
# API JWT Configuration (common)
|
||||||
|
# generate secret with `openssl rand -base64 32`
|
||||||
|
GODOXY_API_JWT_SECRET=
|
||||||
|
# the JWT token time-to-live
|
||||||
|
# leave empty to use default (24 hours)
|
||||||
|
# format: https://pkg.go.dev/time#Duration
|
||||||
|
GODOXY_API_JWT_TOKEN_TTL=
|
||||||
|
|
||||||
# API/WebUI user password login credentials (optional)
|
# API/WebUI user password login credentials (optional)
|
||||||
# These fields are not required for OIDC authentication
|
# These fields are not required for OIDC authentication
|
||||||
GODOXY_API_USER=admin
|
GODOXY_API_USER=admin
|
||||||
GODOXY_API_PASSWORD=password
|
GODOXY_API_PASSWORD=password
|
||||||
# generate secret with `openssl rand -base64 32`
|
|
||||||
GODOXY_API_JWT_SECRET=
|
|
||||||
# the JWT token time-to-live
|
|
||||||
GODOXY_API_JWT_TOKEN_TTL=1h
|
|
||||||
|
|
||||||
# OIDC Configuration (optional)
|
# OIDC Configuration (optional)
|
||||||
# Uncomment and configure these values to enable OIDC authentication.
|
# Uncomment and configure these values to enable OIDC authentication.
|
||||||
|
#
|
||||||
# GODOXY_OIDC_ISSUER_URL=https://accounts.google.com
|
# GODOXY_OIDC_ISSUER_URL=https://accounts.google.com
|
||||||
# GODOXY_OIDC_CLIENT_ID=your-client-id
|
# GODOXY_OIDC_CLIENT_ID=your-client-id
|
||||||
# GODOXY_OIDC_CLIENT_SECRET=your-client-secret
|
# GODOXY_OIDC_CLIENT_SECRET=your-client-secret
|
||||||
# Keep /api/auth/callback as the redirect URL, change the domain to match your setup.
|
# GODOXY_OIDC_SCOPES=openid, profile, email, groups # you may also include `offline_access` if your Idp supports it (e.g. Authentik, Pocket ID)
|
||||||
# GODOXY_OIDC_REDIRECT_URL=https://your-domain/api/auth/callback
|
|
||||||
# Comma-separated list of scopes
|
|
||||||
# GODOXY_OIDC_SCOPES=openid, profile, email
|
|
||||||
#
|
#
|
||||||
# User definitions: Uncomment and configure these values to restrict access to specific users or groups.
|
# User definitions: Uncomment and configure these values to restrict access to specific users or groups.
|
||||||
# These two fields act as a logical AND operator. For example, given the following membership:
|
# These two fields act as a logical AND operator. For example, given the following membership:
|
||||||
@@ -45,8 +50,12 @@ GODOXY_API_ADDR=127.0.0.1:8888
|
|||||||
# Frontend listening port
|
# Frontend listening port
|
||||||
GODOXY_FRONTEND_PORT=3000
|
GODOXY_FRONTEND_PORT=3000
|
||||||
|
|
||||||
# Prometheus Metrics
|
# Frontend aliases (subdomains / FQDNs, e.g. godoxy, godoxy.domain.com)
|
||||||
GODOXY_PROMETHEUS_ENABLED=true
|
GODOXY_FRONTEND_ALIASES=godoxy
|
||||||
|
|
||||||
|
# Docker socket
|
||||||
|
# /var/run/podman/podman.sock for podman
|
||||||
|
DOCKER_SOCKET=/var/run/docker.sock
|
||||||
|
|
||||||
# Debug mode
|
# Debug mode
|
||||||
GODOXY_DEBUG=false
|
GODOXY_DEBUG=false
|
||||||
3
.github/workflows/agent-binary.yml
vendored
3
.github/workflows/agent-binary.yml
vendored
@@ -36,9 +36,6 @@ jobs:
|
|||||||
- name: Check binary
|
- name: Check binary
|
||||||
run: |
|
run: |
|
||||||
file bin/${{ matrix.binary_name }}
|
file bin/${{ matrix.binary_name }}
|
||||||
- name: Test
|
|
||||||
run: |
|
|
||||||
go test -v ./agent/...
|
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
14
Dockerfile
14
Dockerfile
@@ -6,14 +6,6 @@ HEALTHCHECK NONE
|
|||||||
# trunk-ignore(hadolint/DL3018)
|
# trunk-ignore(hadolint/DL3018)
|
||||||
RUN apk add --no-cache tzdata make libcap-setcap
|
RUN apk add --no-cache tzdata make libcap-setcap
|
||||||
|
|
||||||
WORKDIR /src
|
|
||||||
|
|
||||||
# Only copy go.mod and go.sum initially for better caching
|
|
||||||
COPY go.mod go.sum /src/
|
|
||||||
|
|
||||||
ENV GOPATH=/root/go
|
|
||||||
RUN go mod download -x
|
|
||||||
|
|
||||||
# Stage 2: builder
|
# Stage 2: builder
|
||||||
FROM deps AS builder
|
FROM deps AS builder
|
||||||
|
|
||||||
@@ -25,6 +17,12 @@ COPY internal ./internal
|
|||||||
COPY pkg ./pkg
|
COPY pkg ./pkg
|
||||||
COPY agent ./agent
|
COPY agent ./agent
|
||||||
|
|
||||||
|
# Only copy go.mod and go.sum initially for better caching
|
||||||
|
COPY go.mod go.sum /src/
|
||||||
|
|
||||||
|
ENV GOPATH=/root/go
|
||||||
|
RUN go mod download -x
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
ENV VERSION=${VERSION}
|
ENV VERSION=${VERSION}
|
||||||
|
|
||||||
|
|||||||
76
Makefile
76
Makefile
@@ -7,10 +7,12 @@ LDFLAGS = -X github.com/yusing/go-proxy/pkg.version=${VERSION}
|
|||||||
|
|
||||||
ifeq ($(agent), 1)
|
ifeq ($(agent), 1)
|
||||||
NAME = godoxy-agent
|
NAME = godoxy-agent
|
||||||
CMD_PATH = ./agent/cmd
|
CMD_PATH = ./cmd
|
||||||
|
PWD = ${shell pwd}/agent
|
||||||
else
|
else
|
||||||
NAME = godoxy
|
NAME = godoxy
|
||||||
CMD_PATH = ./cmd
|
CMD_PATH = ./cmd
|
||||||
|
PWD = ${shell pwd}
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(trace), 1)
|
ifeq ($(trace), 1)
|
||||||
@@ -27,21 +29,20 @@ endif
|
|||||||
ifeq ($(debug), 1)
|
ifeq ($(debug), 1)
|
||||||
CGO_ENABLED = 0
|
CGO_ENABLED = 0
|
||||||
GODOXY_DEBUG = 1
|
GODOXY_DEBUG = 1
|
||||||
BUILD_FLAGS += -gcflags=all='-N -l'
|
BUILD_FLAGS += -gcflags=all='-N -l' -tags debug
|
||||||
endif
|
else ifeq ($(pprof), 1)
|
||||||
|
|
||||||
ifeq ($(pprof), 1)
|
|
||||||
CGO_ENABLED = 1
|
CGO_ENABLED = 1
|
||||||
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
||||||
BUILD_FLAGS = -tags pprof
|
BUILD_FLAGS += -tags pprof
|
||||||
VERSION := ${VERSION}-pprof
|
VERSION := ${VERSION}-pprof
|
||||||
else
|
else
|
||||||
CGO_ENABLED = 0
|
CGO_ENABLED = 0
|
||||||
LDFLAGS += -s -w
|
LDFLAGS += -s -w
|
||||||
BUILD_FLAGS = -pgo=auto -tags production
|
BUILD_FLAGS += -pgo=auto -tags production
|
||||||
endif
|
endif
|
||||||
|
|
||||||
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
|
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
|
||||||
|
BIN_PATH := $(shell pwd)/bin/${NAME}
|
||||||
|
|
||||||
export NAME
|
export NAME
|
||||||
export CMD_PATH
|
export CMD_PATH
|
||||||
@@ -52,23 +53,34 @@ export GODEBUG
|
|||||||
export GORACE
|
export GORACE
|
||||||
export BUILD_FLAGS
|
export BUILD_FLAGS
|
||||||
|
|
||||||
|
ifeq ($(shell id -u), 0)
|
||||||
|
SETCAP_CMD = setcap
|
||||||
|
else
|
||||||
|
SETCAP_CMD = sudo setcap
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: debug
|
||||||
|
|
||||||
test:
|
test:
|
||||||
GODOXY_TEST=1 go test ./internal/...
|
GODOXY_TEST=1 go test ./internal/...
|
||||||
|
|
||||||
get:
|
get:
|
||||||
go get -u ./cmd && go mod tidy
|
for dir in ${PWD} ${PWD}/agent; do cd $$dir && go get -u ./... && go mod tidy; done
|
||||||
|
|
||||||
build:
|
build:
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
go build ${BUILD_FLAGS} -o bin/${NAME} ${CMD_PATH}
|
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ${CMD_PATH}
|
||||||
if [ $(shell id -u) -eq 0 ]; \
|
|
||||||
then setcap CAP_NET_BIND_SERVICE=+eip bin/${NAME}; \
|
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
|
||||||
else sudo setcap CAP_NET_BIND_SERVICE=+eip bin/${NAME}; \
|
$(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH}
|
||||||
fi
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
[ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ${CMD_PATH}
|
[ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ${CMD_PATH}
|
||||||
|
|
||||||
|
debug:
|
||||||
|
make NAME="godoxy-test" debug=1 build
|
||||||
|
sh -c 'HTTP_ADDR=:81 HTTPS_ADDR=:8443 API_ADDR=:8899 DEBUG=1 bin/godoxy-test'
|
||||||
|
|
||||||
mtrace:
|
mtrace:
|
||||||
bin/godoxy debug-ls-mtrace > mtrace.json
|
bin/godoxy debug-ls-mtrace > mtrace.json
|
||||||
|
|
||||||
@@ -90,43 +102,5 @@ cloc:
|
|||||||
link-binary:
|
link-binary:
|
||||||
ln -s /app/${NAME} bin/run
|
ln -s /app/${NAME} bin/run
|
||||||
|
|
||||||
# To generate schema
|
|
||||||
# comment out this part from typescript-json-schema.js#L884
|
|
||||||
#
|
|
||||||
# if (indexType.flags !== ts.TypeFlags.Number && !isIndexedObject) {
|
|
||||||
# throw new Error("Not supported: IndexSignatureDeclaration with index symbol other than a number or a string");
|
|
||||||
# }
|
|
||||||
|
|
||||||
gen-schema-single:
|
|
||||||
bun --bun run typescript-json-schema --noExtraProps --required --skipLibCheck --tsNodeRegister=true -o schemas/${OUT} schemas/${IN} ${CLASS}
|
|
||||||
# minify
|
|
||||||
python3 -c "import json; f=open('schemas/${OUT}', 'r'); j=json.load(f); f.close(); f=open('schemas/${OUT}', 'w'); json.dump(j, f, separators=(',', ':'));"
|
|
||||||
|
|
||||||
gen-schema:
|
|
||||||
cd schemas && bun --bun tsc
|
|
||||||
make IN=config/config.ts \
|
|
||||||
CLASS=Config \
|
|
||||||
OUT=config.schema.json \
|
|
||||||
gen-schema-single
|
|
||||||
make IN=providers/routes.ts \
|
|
||||||
CLASS=Routes \
|
|
||||||
OUT=routes.schema.json \
|
|
||||||
gen-schema-single
|
|
||||||
make IN=middlewares/middleware_compose.ts \
|
|
||||||
CLASS=MiddlewareCompose \
|
|
||||||
OUT=middleware_compose.schema.json \
|
|
||||||
gen-schema-single
|
|
||||||
make IN=docker.ts \
|
|
||||||
CLASS=DockerRoutes \
|
|
||||||
OUT=docker_routes.schema.json \
|
|
||||||
gen-schema-single
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
publish-schema:
|
|
||||||
cd schemas && bun publish && cd ..
|
|
||||||
|
|
||||||
update-schema-generator:
|
|
||||||
pnpm up -g typescript-json-schema
|
|
||||||
|
|
||||||
push-github:
|
push-github:
|
||||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
108
README.md
108
README.md
@@ -5,6 +5,7 @@
|
|||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_godoxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_godoxy)
|
||||||

|

|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_godoxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_godoxy)
|
||||||
|

|
||||||
[](https://discord.gg/umReR62nRd)
|
[](https://discord.gg/umReR62nRd)
|
||||||
|
|
||||||
A lightweight, simple, and [performant](https://github.com/yusing/godoxy/wiki/Benchmarks) reverse proxy with WebUI.
|
A lightweight, simple, and [performant](https://github.com/yusing/godoxy/wiki/Benchmarks) reverse proxy with WebUI.
|
||||||
@@ -13,10 +14,6 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
|
|||||||
|
|
||||||
**EN** | <a href="README_CHT.md">中文</a>
|
**EN** | <a href="README_CHT.md">中文</a>
|
||||||
|
|
||||||
<!-- [](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy) -->
|
|
||||||
|
|
||||||
<img src="screenshots/webui.jpg" style="max-width: 650">
|
<img src="screenshots/webui.jpg" style="max-width: 650">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -27,10 +24,11 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
|
|||||||
|
|
||||||
- [GoDoxy](#godoxy)
|
- [GoDoxy](#godoxy)
|
||||||
- [Table of content](#table-of-content)
|
- [Table of content](#table-of-content)
|
||||||
|
- [Running demo](#running-demo)
|
||||||
- [Key Features](#key-features)
|
- [Key Features](#key-features)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [How does GoDoxy work](#how-does-godoxy-work)
|
|
||||||
- [Setup](#setup)
|
- [Setup](#setup)
|
||||||
|
- [How does GoDoxy work](#how-does-godoxy-work)
|
||||||
- [Screenshots](#screenshots)
|
- [Screenshots](#screenshots)
|
||||||
- [idlesleeper](#idlesleeper)
|
- [idlesleeper](#idlesleeper)
|
||||||
- [Metrics and Logs](#metrics-and-logs)
|
- [Metrics and Logs](#metrics-and-logs)
|
||||||
@@ -38,32 +36,71 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
|
|||||||
- [Folder structrue](#folder-structrue)
|
- [Folder structrue](#folder-structrue)
|
||||||
- [Build it yourself](#build-it-yourself)
|
- [Build it yourself](#build-it-yourself)
|
||||||
|
|
||||||
|
## Running demo
|
||||||
|
|
||||||
|
<https://demo.godoxy.dev>
|
||||||
|
|
||||||
|
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- Easy to use
|
- **Simple**
|
||||||
- Effortless configuration
|
- Effortless configuration with [simple labels](https://github.com/yusing/godoxy/wiki/Docker-labels-and-Route-Files) or WebUI
|
||||||
- Simple multi-node setup with GoDoxy agents or Docker Socket Proxies
|
- [Simple multi-node setup](https://github.com/yusing/godoxy/wiki/Configurations#multi-docker-nodes-setup)
|
||||||
- Error messages is clear and detailed, easy troubleshooting
|
- Detailed error messages for easy troubleshooting.
|
||||||
- Auto SSL with Let's Encrypt (See [Supported DNS-01 Challenge Providers](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
- **ACL**: connection / request level access control
|
||||||
- Auto hot-reload on container state / config file changes
|
- IP/CIDR
|
||||||
- Container aware: create routes dynamically from running docker containers
|
- Country **(Maxmind account required)**
|
||||||
- **idlesleeper**: stop and wake containers based on traffic _(optional, see [screenshots](#idlesleeper))_
|
- Timezone **(Maxmind account required)**
|
||||||
- HTTP reserve proxy and TCP/UDP port forwarding
|
- **Access logging**
|
||||||
- OpenID Connect integration: SSO and secure your apps easily
|
- **Advanced Automation**
|
||||||
- [HTTP middleware](https://github.com/yusing/go-proxy/wiki/Middlewares) and [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
||||||
- **Web UI with App dashboard, config editor, _uptime and system metrics_, _docker logs viewer_**
|
- Auto-configuration for Docker containers
|
||||||
- Supports linux/amd64 and linux/arm64
|
- Hot-reloading of configurations and container state changes
|
||||||
- Written in **[Go](https://go.dev)**
|
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
||||||
|
- Docker containers
|
||||||
[🔼Back to top](#table-of-content)
|
- Proxmox LXCs
|
||||||
|
- **Traffic Management**
|
||||||
|
- HTTP reserve proxy
|
||||||
|
- TCP/UDP port forwarding
|
||||||
|
- **OpenID Connect support**: SSO and secure your apps easily
|
||||||
|
- **Customization**
|
||||||
|
- [HTTP middlewares](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||||
|
- [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||||
|
- **Web UI**
|
||||||
|
- App Dashboard
|
||||||
|
- Config Editor
|
||||||
|
- Uptime and System Metrics
|
||||||
|
- Docker Logs Viewer
|
||||||
|
- **Cross-Platform support**
|
||||||
|
- Supports **linux/amd64** and **linux/arm64**
|
||||||
|
- **Efficient and Performant**
|
||||||
|
- Written in **[Go](https://go.dev)**
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Setup Wildcard DNS Record(s) for machine running `GoDoxy`, e.g.
|
Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
||||||
|
|
||||||
- A Record: `*.domain.com` -> `10.0.10.1`
|
- A Record: `*.domain.com` -> `10.0.10.1`
|
||||||
- AAAA Record (if you use IPv6): `*.domain.com` -> `::ffff:a00:a01`
|
- AAAA Record (if you use IPv6): `*.domain.com` -> `::ffff:a00:a01`
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> GoDoxy is designed to be running in `host` network mode, do not change it.
|
||||||
|
>
|
||||||
|
> To change listening ports, modify `.env`.
|
||||||
|
|
||||||
|
1. Prepare a new directory for docker compose and config files.
|
||||||
|
|
||||||
|
2. Run setup script inside the directory, or [set up manually](#manual-setup)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
||||||
|
|
||||||
## How does GoDoxy work
|
## How does GoDoxy work
|
||||||
|
|
||||||
1. List all the containers
|
1. List all the containers
|
||||||
@@ -71,27 +108,10 @@ Setup Wildcard DNS Record(s) for machine running `GoDoxy`, e.g.
|
|||||||
3. Create a route if applicable (a route is like a "Virtual Host" in NPM)
|
3. Create a route if applicable (a route is like a "Virtual Host" in NPM)
|
||||||
4. Watch for container / config changes and update automatically
|
4. Watch for container / config changes and update automatically
|
||||||
|
|
||||||
GoDoxy uses the label `proxy.aliases` as the subdomain(s), if unset it defaults to the `container_name` field in docker compose.
|
> [!NOTE]
|
||||||
|
> GoDoxy uses the label `proxy.aliases` as the subdomain(s), if unset it defaults to the `container_name` field in docker compose.
|
||||||
For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
>
|
||||||
|
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
||||||
## Setup
|
|
||||||
|
|
||||||
**NOTE:** GoDoxy is designed to be (and only works when) running in `host` network mode, do not change it. To change listening ports, modify `.env`.
|
|
||||||
|
|
||||||
1. Prepare a new directory for docker compose and config files.
|
|
||||||
|
|
||||||
2. Run setup script inside the directory, or [set up manually](#manual-setup)
|
|
||||||
|
|
||||||
```shell
|
|
||||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Start the container `docker compose up -d` and wait for it to be ready
|
|
||||||
|
|
||||||
4. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -124,8 +144,6 @@ For example, with the label `proxy.aliases: qbt` you can access your app via `qb
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Manual Setup
|
## Manual Setup
|
||||||
|
|
||||||
1. Make `config` directory then grab `config.example.yml` into `config/config.yml`
|
1. Make `config` directory then grab `config.example.yml` into `config/config.yml`
|
||||||
|
|||||||
@@ -5,16 +5,13 @@
|
|||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||

|

|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||
[](https://discord.gg/umReR62nRd)
|

|
||||||
|
[](https://discord.gg/umReR62nRd)
|
||||||
|
|
||||||
輕量、易用、 [高效能](https://github.com/yusing/godoxy/wiki/Benchmarks),且帶有主頁和配置面板的反向代理
|
輕量、易用、 [高效能](https://github.com/yusing/godoxy/wiki/Benchmarks),且帶有主頁和配置面板的反向代理
|
||||||
|
|
||||||
完整文檔請查閱 **[Wiki](https://github.com/yusing/godoxy/wiki)**(暫未有中文翻譯)
|
完整文檔請查閱 **[Wiki](https://github.com/yusing/godoxy/wiki)**(暫未有中文翻譯)
|
||||||
|
|
||||||
<!-- [](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy) -->
|
|
||||||
|
|
||||||
<a href="README.md">EN</a> | **中文**
|
<a href="README.md">EN</a> | **中文**
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
||||||
@@ -27,6 +24,7 @@
|
|||||||
|
|
||||||
- [GoDoxy](#godoxy)
|
- [GoDoxy](#godoxy)
|
||||||
- [目錄](#目錄)
|
- [目錄](#目錄)
|
||||||
|
- [運行示例](#運行示例)
|
||||||
- [主要特點](#主要特點)
|
- [主要特點](#主要特點)
|
||||||
- [前置需求](#前置需求)
|
- [前置需求](#前置需求)
|
||||||
- [安裝](#安裝)
|
- [安裝](#安裝)
|
||||||
@@ -37,6 +35,12 @@
|
|||||||
- [監控](#監控)
|
- [監控](#監控)
|
||||||
- [自行編譯](#自行編譯)
|
- [自行編譯](#自行編譯)
|
||||||
|
|
||||||
|
## 運行示例
|
||||||
|
|
||||||
|
<https://demo.godoxy.dev>
|
||||||
|
|
||||||
|
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||||
|
|
||||||
## 主要特點
|
## 主要特點
|
||||||
|
|
||||||
- 容易使用
|
- 容易使用
|
||||||
@@ -48,13 +52,13 @@
|
|||||||
- 容器狀態/配置文件變更時自動熱重載
|
- 容器狀態/配置文件變更時自動熱重載
|
||||||
- **閒置休眠**:在閒置時停止容器,有流量時喚醒(_可選,參見[截圖](#閒置休眠)_)
|
- **閒置休眠**:在閒置時停止容器,有流量時喚醒(_可選,參見[截圖](#閒置休眠)_)
|
||||||
- OpenID Connect:輕鬆實現單點登入
|
- OpenID Connect:輕鬆實現單點登入
|
||||||
- HTTP(s) 反向代理和TCP 和 UDP 埠轉發
|
- HTTP(s) 反向代理和 TCP 和 UDP 埠轉發
|
||||||
- [HTTP 中介軟體](https://github.com/yusing/godoxy/wiki/Middlewares) 和 [自定義錯誤頁面](https://github.com/yusing/godoxy/wiki/Middlewares#custom-error-pages)
|
- [HTTP 中介軟體](https://github.com/yusing/godoxy/wiki/Middlewares) 和 [自定義錯誤頁面](https://github.com/yusing/godoxy/wiki/Middlewares#custom-error-pages)
|
||||||
- **網頁介面,具有應用儀表板和配置編輯器**
|
- **網頁介面,具有應用儀表板和配置編輯器**
|
||||||
- 支援 linux/amd64、linux/arm64
|
- 支援 linux/amd64、linux/arm64
|
||||||
- 使用 **[Go](https://go.dev)** 編寫
|
- 使用 **[Go](https://go.dev)** 編寫
|
||||||
|
|
||||||
[🔼回到頂部](#目錄)
|
[🔼 回到頂部](#目錄)
|
||||||
|
|
||||||
## 前置需求
|
## 前置需求
|
||||||
|
|
||||||
@@ -65,21 +69,22 @@
|
|||||||
|
|
||||||
## 安裝
|
## 安裝
|
||||||
|
|
||||||
**注意:** GoDoxy 設計為(且僅在)`host` 網路模式下運作,請勿更改。如需更改監聽埠,請修改 `.env`。
|
> [!NOTE]
|
||||||
|
> GoDoxy 僅在 `host` 網路模式下運作,請勿更改。
|
||||||
|
>
|
||||||
|
> 如需更改監聽埠,請修改 `.env`。
|
||||||
|
|
||||||
1. 準備一個新目錄用於 docker compose 和配置文件。
|
1. 準備一個新目錄用於 docker compose 和配置文件。
|
||||||
|
|
||||||
2. 在目錄內運行安裝腳本,或[手動安裝](#手動安裝)
|
2. 在目錄內運行安裝腳本,或[手動安裝](#手動安裝)
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 啟動容器 `docker compose up -d` 並等待就緒
|
3. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
|
||||||
|
|
||||||
4. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
|
[🔼 回到頂部](#目錄)
|
||||||
|
|
||||||
[🔼回到頂部](#目錄)
|
|
||||||
|
|
||||||
### 手動安裝
|
### 手動安裝
|
||||||
|
|
||||||
@@ -122,7 +127,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
[🔼回到頂部](#目錄)
|
[🔼 回到頂部](#目錄)
|
||||||
|
|
||||||
### 監控
|
### 監控
|
||||||
|
|
||||||
@@ -161,4 +166,4 @@
|
|||||||
|
|
||||||
5. 使用 `make build` 編譯二進制檔案
|
5. 使用 `make build` 編譯二進制檔案
|
||||||
|
|
||||||
[🔼回到頂部](#目錄)
|
[🔼 回到頂部](#目錄)
|
||||||
|
|||||||
95
agent/go.mod
Normal file
95
agent/go.mod
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
module github.com/yusing/go-proxy/agent
|
||||||
|
|
||||||
|
go 1.24.2
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy => ..
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/coder/websocket v1.8.13
|
||||||
|
github.com/docker/docker v28.1.1+incompatible
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
github.com/yusing/go-proxy v0.11.5
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250418000134-7af8fd7b079e
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
|
github.com/bytedance/sonic v1.13.2 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/diskfs/go-diskfs v1.6.0 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/djherbis/times v1.6.0 // indirect
|
||||||
|
github.com/docker/cli v28.1.1+incompatible // indirect
|
||||||
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
|
github.com/go-acme/lego/v4 v4.23.1 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.17.1 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/gotify/server/v2 v2.6.3 // indirect
|
||||||
|
github.com/jinzhu/copier v0.4.0 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||||
|
github.com/luthermonson/go-proxmox v0.2.2 // indirect
|
||||||
|
github.com/magefile/mage v1.15.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/miekg/dns v1.1.65 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.51.0 // indirect
|
||||||
|
github.com/samber/lo v1.50.0 // indirect
|
||||||
|
github.com/samber/slog-common v0.18.1 // indirect
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.3 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||||
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.1 // indirect
|
||||||
|
golang.org/x/arch v0.16.0 // indirect
|
||||||
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
|
golang.org/x/net v0.39.0 // indirect
|
||||||
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
|
golang.org/x/text v0.24.0 // indirect
|
||||||
|
golang.org/x/time v0.11.0 // indirect
|
||||||
|
golang.org/x/tools v0.32.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
360
agent/go.sum
Normal file
360
agent/go.sum
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||||
|
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
|
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||||
|
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
||||||
|
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||||
|
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||||
|
github.com/coder/websocket v1.8.13/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/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
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/diskfs/go-diskfs v1.6.0 h1:YmK5+vLSfkwC6kKKRTRPGaDGNF+Xh8FXeiNHwryDfu4=
|
||||||
|
github.com/diskfs/go-diskfs v1.6.0/go.mod h1:bRFumZeGFCO8C2KNswrQeuj2m1WCVr4Ms5IjWMczMDk=
|
||||||
|
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/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
|
github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=
|
||||||
|
github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||||
|
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
|
||||||
|
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
|
github.com/go-acme/lego/v4 v4.23.1 h1:lZ5fGtGESA2L9FB8dNTvrQUq3/X4QOb8ExkKyY7LSV4=
|
||||||
|
github.com/go-acme/lego/v4 v4.23.1/go.mod h1:7UMVR7oQbIYw6V7mTgGwi4Er7B6Ww0c+c8feiBM0EgI=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
|
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||||
|
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
|
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||||
|
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/godoxy-app/docker v0.0.0-20250418000134-7af8fd7b079e h1:LEbMtJ6loEubxetD+Aw8+1x0rShor5iMoy9WuFQ8hN8=
|
||||||
|
github.com/godoxy-app/docker v0.0.0-20250418000134-7af8fd7b079e/go.mod h1:3tMTnTkH7IN5smn7PX83XdmRnNj4Nw2/Pt8GgReqnKM=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
|
||||||
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I=
|
||||||
|
github.com/gotify/server/v2 v2.6.3/go.mod h1:IyeQ/iL3vetcuqUAzkCMVObIMGGJx4zb13/mVatIwE8=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
|
||||||
|
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||||
|
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||||
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||||
|
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||||
|
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
|
github.com/luthermonson/go-proxmox v0.2.2 h1:BZ7VEj302wxw2i/EwTcyEiBzQib8teocB2SSkLHyySY=
|
||||||
|
github.com/luthermonson/go-proxmox v0.2.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||||
|
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||||
|
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
|
||||||
|
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
|
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||||
|
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
||||||
|
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||||
|
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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
|
||||||
|
github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
|
||||||
|
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
|
||||||
|
github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ=
|
||||||
|
github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk=
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.7.3 h1:/MkPDl/tJhijN2GvB1MWwBn2FU8RiL3rQ8gpXkQm2EY=
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.7.3/go.mod h1:oWU7WHof4Xp8VguiNO02r1a4VzkgoOyOZhY5CuRke60=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
|
||||||
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||||
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||||
|
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||||
|
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||||
|
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
|
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||||
|
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
|
||||||
|
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||||
|
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||||
|
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||||
|
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
|
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
|
||||||
|
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
|
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
||||||
|
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
|
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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
|
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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
|
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||||
|
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
|
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
|
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||||
|
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
|
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||||
|
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||||
|
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-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f h1:tjZsroqekhC63+WMqzmWyW5Twj/ZfR5HAlpd5YQ1Vs0=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197 h1:29cjnHVylHwTzH66WfFZqgSQgnxzvWE+jvBwpZCLRxY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
|
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||||
|
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||||
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,7 +17,6 @@ import (
|
|||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
|
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/net/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/pkg"
|
"github.com/yusing/go-proxy/pkg"
|
||||||
)
|
)
|
||||||
@@ -49,9 +49,17 @@ const (
|
|||||||
FakeDockerHostPrefixLen = len(FakeDockerHostPrefix)
|
FakeDockerHostPrefixLen = len(FakeDockerHostPrefix)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func mustParseURL(urlStr string) *url.URL {
|
||||||
|
u, err := url.Parse(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
AgentURL = types.MustParseURL(APIBaseURL)
|
AgentURL = mustParseURL(APIBaseURL)
|
||||||
HTTPProxyURL = types.MustParseURL(APIBaseURL + EndpointProxyHTTP)
|
HTTPProxyURL = mustParseURL(APIBaseURL + EndpointProxyHTTP)
|
||||||
HTTPProxyURLPrefixLen = len(APIEndpointBase + EndpointProxyHTTP)
|
HTTPProxyURLPrefixLen = len(APIEndpointBase + EndpointProxyHTTP)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,14 +80,6 @@ func (cfg *AgentConfig) Parse(addr string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func withoutBuildTime(version string) string {
|
|
||||||
return strings.Split(version, "-")[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkVersion(a, b string) bool {
|
|
||||||
return withoutBuildTime(a) == withoutBuildTime(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte) error {
|
func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte) error {
|
||||||
clientCert, err := tls.X509KeyPair(crt, key)
|
clientCert, err := tls.X509KeyPair(crt, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -105,18 +105,6 @@ func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte)
|
|||||||
ctx, cancel := context.WithTimeout(parent.Context(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(parent.Context(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// check agent version
|
|
||||||
version, _, err := cfg.Fetch(ctx, EndpointVersion)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
versionStr := string(version)
|
|
||||||
// skip version check for dev versions
|
|
||||||
if strings.HasPrefix(versionStr, "v") && !checkVersion(versionStr, pkg.GetVersion()) {
|
|
||||||
return gperr.Errorf("agent version mismatch: server: %s, agent: %s", pkg.GetVersion(), versionStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get agent name
|
// get agent name
|
||||||
name, _, err := cfg.Fetch(ctx, EndpointName)
|
name, _, err := cfg.Fetch(ctx, EndpointName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -124,8 +112,21 @@ func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte)
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg.name = string(name)
|
cfg.name = string(name)
|
||||||
|
|
||||||
cfg.l = logging.With().Str("agent", cfg.name).Logger()
|
cfg.l = logging.With().Str("agent", cfg.name).Logger()
|
||||||
|
|
||||||
|
// check agent version
|
||||||
|
agentVersionBytes, _, err := cfg.Fetch(ctx, EndpointVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
agentVersion := string(agentVersionBytes)
|
||||||
|
|
||||||
|
if pkg.GetVersion().IsNewerMajorThan(pkg.ParseVersion(agentVersion)) {
|
||||||
|
logging.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.name, pkg.GetVersion(), agentVersion)
|
||||||
|
}
|
||||||
|
|
||||||
logging.Info().Msgf("agent %q initialized", cfg.name)
|
logging.Info().Msgf("agent %q initialized", cfg.name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const AgentCertsBasePath = "certs"
|
||||||
|
|
||||||
func writeFile(zipWriter *zip.Writer, name string, data []byte) error {
|
func writeFile(zipWriter *zip.Writer, name string, data []byte) error {
|
||||||
w, err := zipWriter.CreateHeader(&zip.FileHeader{
|
w, err := zipWriter.CreateHeader(&zip.FileHeader{
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -59,7 +60,7 @@ func AgentCertsFilepath(host string) (filepathOut string, ok bool) {
|
|||||||
if !isValidAgentHost(host) {
|
if !isValidAgentHost(host) {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
return filepath.Join(common.AgentCertsBasePath, host+".zip"), true
|
return filepath.Join(AgentCertsBasePath, host+".zip"), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExtractCert(data []byte) (ca, crt, key []byte, err error) {
|
func ExtractCert(data []byte) (ca, crt, key []byte, err error) {
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
package certs
|
package certs_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/go-proxy/agent/pkg/certs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestZipCert(t *testing.T) {
|
func TestZipCert(t *testing.T) {
|
||||||
ca, crt, key := []byte("test1"), []byte("test2"), []byte("test3")
|
ca, crt, key := []byte("test1"), []byte("test2"), []byte("test3")
|
||||||
zipData, err := ZipCert(ca, crt, key)
|
zipData, err := certs.ZipCert(ca, crt, key)
|
||||||
ExpectNoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ca2, crt2, key2, err := ExtractCert(zipData)
|
ca2, crt2, key2, err := certs.ExtractCert(zipData)
|
||||||
ExpectNoError(t, err)
|
require.NoError(t, err)
|
||||||
ExpectEqual(t, ca, ca2)
|
require.Equal(t, ca, ca2)
|
||||||
ExpectEqual(t, crt, crt2)
|
require.Equal(t, crt, crt2)
|
||||||
ExpectEqual(t, key, key2)
|
require.Equal(t, key, key2)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/net/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
||||||
)
|
)
|
||||||
@@ -44,11 +43,11 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err = monitor.NewHTTPHealthChecker(types.NewURL(&url.URL{
|
result, err = monitor.NewHTTPHealthMonitor(&url.URL{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
Host: host,
|
Host: host,
|
||||||
Path: path,
|
Path: path,
|
||||||
}), defaultHealthConfig).CheckHealth()
|
}, defaultHealthConfig).CheckHealth()
|
||||||
case "tcp", "udp":
|
case "tcp", "udp":
|
||||||
host := query.Get("host")
|
host := query.Get("host")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
@@ -63,10 +62,10 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err = monitor.NewRawHealthChecker(types.NewURL(&url.URL{
|
result, err = monitor.NewRawHealthMonitor(&url.URL{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
Host: host,
|
Host: host,
|
||||||
}), defaultHealthConfig).CheckHealth()
|
}, defaultHealthConfig).CheckHealth()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import (
|
|||||||
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/env"
|
"github.com/yusing/go-proxy/agent/pkg/env"
|
||||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
|
||||||
"github.com/yusing/go-proxy/internal/logging/memlogger"
|
"github.com/yusing/go-proxy/internal/logging/memlogger"
|
||||||
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
"github.com/yusing/go-proxy/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServeMux struct{ *http.ServeMux }
|
type ServeMux struct{ *http.ServeMux }
|
||||||
@@ -37,7 +37,7 @@ func NewAgentHandler() http.Handler {
|
|||||||
mux := ServeMux{http.NewServeMux()}
|
mux := ServeMux{http.NewServeMux()}
|
||||||
|
|
||||||
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
|
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
|
||||||
mux.HandleMethods("GET", agent.EndpointVersion, v1.GetVersion)
|
mux.HandleMethods("GET", agent.EndpointVersion, pkg.GetVersionHTTPHandler())
|
||||||
mux.HandleMethods("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleMethods("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, env.AgentName)
|
fmt.Fprint(w, env.AgentName)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,13 +13,12 @@ import (
|
|||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
||||||
"github.com/yusing/go-proxy/internal/net/types"
|
"github.com/yusing/go-proxy/internal/net/types"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
|
func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
host := r.Header.Get(agentproxy.HeaderXProxyHost)
|
host := r.Header.Get(agentproxy.HeaderXProxyHost)
|
||||||
isHTTPS := strutils.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
|
isHTTPS, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
|
||||||
skipTLSVerify := strutils.ParseBool(r.Header.Get(agentproxy.HeaderXProxySkipTLSVerify))
|
skipTLSVerify, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxySkipTLSVerify))
|
||||||
responseHeaderTimeout, err := strconv.Atoi(r.Header.Get(agentproxy.HeaderXProxyResponseHeaderTimeout))
|
responseHeaderTimeout, err := strconv.Atoi(r.Header.Get(agentproxy.HeaderXProxyResponseHeaderTimeout))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
responseHeaderTimeout = 0
|
responseHeaderTimeout = 0
|
||||||
|
|||||||
@@ -40,5 +40,5 @@ func StartAgentServer(parent task.Parent, opt Options) {
|
|||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Start(parent, agentServer, logger)
|
server.Start(parent, agentServer, nil, logger)
|
||||||
}
|
}
|
||||||
|
|||||||
16
cmd/main.go
16
cmd/main.go
@@ -6,12 +6,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal"
|
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/query"
|
"github.com/yusing/go-proxy/internal/api/v1/query"
|
||||||
|
"github.com/yusing/go-proxy/internal/auth"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/config"
|
"github.com/yusing/go-proxy/internal/config"
|
||||||
|
"github.com/yusing/go-proxy/internal/dnsproviders"
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/homepage"
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
@@ -19,7 +18,7 @@ import (
|
|||||||
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
||||||
"github.com/yusing/go-proxy/internal/metrics/uptime"
|
"github.com/yusing/go-proxy/internal/metrics/uptime"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
||||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/pkg"
|
"github.com/yusing/go-proxy/pkg"
|
||||||
)
|
)
|
||||||
@@ -40,6 +39,7 @@ func parallel(fns ...func()) {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
initProfiling()
|
initProfiling()
|
||||||
|
dnsproviders.InitProviders()
|
||||||
args := pkg.GetArgs(common.MainServerCommandValidator{})
|
args := pkg.GetArgs(common.MainServerCommandValidator{})
|
||||||
|
|
||||||
switch args.Command {
|
switch args.Command {
|
||||||
@@ -50,7 +50,7 @@ func main() {
|
|||||||
rawLogger.Println("ok")
|
rawLogger.Println("ok")
|
||||||
return
|
return
|
||||||
case common.CommandListIcons:
|
case common.CommandListIcons:
|
||||||
icons, err := internal.ListAvailableIcons()
|
icons, err := homepage.ListAvailableIcons()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rawLogger.Fatal(err)
|
rawLogger.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -79,9 +79,7 @@ func main() {
|
|||||||
logging.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
|
logging.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
|
||||||
logging.Trace().Msg("trace enabled")
|
logging.Trace().Msg("trace enabled")
|
||||||
parallel(
|
parallel(
|
||||||
internal.InitIconListCache,
|
homepage.InitIconListCache,
|
||||||
homepage.InitOverridesConfig,
|
|
||||||
favicon.InitIconCache,
|
|
||||||
systeminfo.Poller.Start,
|
systeminfo.Poller.Start,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,7 +119,7 @@ func main() {
|
|||||||
switch args.Command {
|
switch args.Command {
|
||||||
case common.CommandListRoutes:
|
case common.CommandListRoutes:
|
||||||
cfg.StartProxyProviders()
|
cfg.StartProxyProviders()
|
||||||
printJSON(routequery.RoutesByAlias())
|
printJSON(routes.ByAlias())
|
||||||
return
|
return
|
||||||
case common.CommandListConfigs:
|
case common.CommandListConfigs:
|
||||||
printJSON(cfg.Value())
|
printJSON(cfg.Value())
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build production
|
//go:build !pprof
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
frontend:
|
frontend:
|
||||||
image: ghcr.io/yusing/godoxy-frontend:latest
|
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
|
||||||
container_name: godoxy-frontend
|
container_name: godoxy-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host # do not change this
|
network_mode: host # do not change this
|
||||||
@@ -13,7 +13,7 @@ services:
|
|||||||
|
|
||||||
# modify below to fit your needs
|
# modify below to fit your needs
|
||||||
labels:
|
labels:
|
||||||
proxy.aliases: godoxy
|
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
|
||||||
proxy.godoxy.port: ${GODOXY_FRONTEND_PORT:-3000}
|
proxy.godoxy.port: ${GODOXY_FRONTEND_PORT:-3000}
|
||||||
# proxy.godoxy.middlewares.cidr_whitelist: |
|
# proxy.godoxy.middlewares.cidr_whitelist: |
|
||||||
# status: 403
|
# status: 403
|
||||||
@@ -24,13 +24,13 @@ services:
|
|||||||
# - 192.168.0.0/16
|
# - 192.168.0.0/16
|
||||||
# - 172.16.0.0/12
|
# - 172.16.0.0/12
|
||||||
app:
|
app:
|
||||||
image: ghcr.io/yusing/godoxy:latest
|
image: ghcr.io/yusing/godoxy:${TAG:-latest}
|
||||||
container_name: godoxy
|
container_name: godoxy
|
||||||
restart: always
|
restart: always
|
||||||
network_mode: host # do not change this
|
network_mode: host # do not change this
|
||||||
env_file: .env
|
env_file: .env
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
- ./error_pages:/app/error_pages
|
- ./error_pages:/app/error_pages
|
||||||
|
|||||||
@@ -17,6 +17,25 @@
|
|||||||
|
|
||||||
# 3. other providers, see https://github.com/yusing/godoxy/wiki/Supported-DNS%E2%80%9001-Providers#supported-dns-01-providers
|
# 3. other providers, see https://github.com/yusing/godoxy/wiki/Supported-DNS%E2%80%9001-Providers#supported-dns-01-providers
|
||||||
|
|
||||||
|
# acl:
|
||||||
|
# default: allow # or deny (default: allow)
|
||||||
|
# allow_local: true # or false (default: true)
|
||||||
|
# allow:
|
||||||
|
# - ip:1.2.3.4
|
||||||
|
# - cidr:1.2.3.4/32
|
||||||
|
# - country:US
|
||||||
|
# - timezone:Asia/Shanghai
|
||||||
|
# deny:
|
||||||
|
# - ip:1.2.3.4
|
||||||
|
# - cidr:1.2.3.4/32
|
||||||
|
# - country:US
|
||||||
|
# - timezone:Asia/Shanghai
|
||||||
|
# log: # warning: logging ACL can be slow based on the number of incoming connections and configured rules
|
||||||
|
# buffer_size: 65536 # (default: 64KB)
|
||||||
|
# path: /app/logs/acl.log # (default: none)
|
||||||
|
# stdout: false # (default: false)
|
||||||
|
# keep: last 10 # (default: none)
|
||||||
|
|
||||||
entrypoint:
|
entrypoint:
|
||||||
# Below define an example of middleware config
|
# Below define an example of middleware config
|
||||||
# 1. block non local IP connections
|
# 1. block non local IP connections
|
||||||
@@ -73,6 +92,14 @@ providers:
|
|||||||
# url: https://discord.com/api/webhooks/...
|
# url: https://discord.com/api/webhooks/...
|
||||||
# template: discord # this means use payload template from internal/notif/templates/discord.json
|
# template: discord # this means use payload template from internal/notif/templates/discord.json
|
||||||
|
|
||||||
|
# Proxmox providers (for idlesleep support for proxmox LXCs)
|
||||||
|
#
|
||||||
|
# proxmox:
|
||||||
|
# - url: https://pve.domain.com:8006/api2/json
|
||||||
|
# token_id: root@pam!abcdef
|
||||||
|
# secret: aaaa-bbbb-cccc-dddd
|
||||||
|
# no_tls_verify: true
|
||||||
|
|
||||||
# Check https://github.com/yusing/godoxy/wiki/Certificates-and-domain-matching#domain-matching
|
# Check https://github.com/yusing/godoxy/wiki/Certificates-and-domain-matching#domain-matching
|
||||||
# for explaination of `match_domains`
|
# for explaination of `match_domains`
|
||||||
#
|
#
|
||||||
|
|||||||
196
go.mod
196
go.mod
@@ -2,6 +2,10 @@ module github.com/yusing/go-proxy
|
|||||||
|
|
||||||
go 1.24.2
|
go 1.24.2
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy/agent => ./agent
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy/internal/dnsproviders => ./internal/dnsproviders
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
|
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
|
||||||
github.com/coder/websocket v1.8.13 // websocket for API and agent
|
github.com/coder/websocket v1.8.13 // websocket for API and agent
|
||||||
@@ -11,10 +15,8 @@ require (
|
|||||||
github.com/go-acme/lego/v4 v4.23.1 // acme client
|
github.com/go-acme/lego/v4 v4.23.1 // acme client
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // validator
|
github.com/go-playground/validator/v10 v10.26.0 // validator
|
||||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 // jwt for default auth
|
github.com/gotify/server/v2 v2.6.3 // reference the Message struct for json response
|
||||||
github.com/gotify/server/v2 v2.6.1 // reference the Message struct for json response
|
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||||
github.com/prometheus/client_golang v1.22.0 // metrics
|
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // lock free map for concurrent operations
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 // lock free map for concurrent operations
|
||||||
github.com/rs/zerolog v1.34.0 // logging
|
github.com/rs/zerolog v1.34.0 // logging
|
||||||
github.com/shirou/gopsutil/v4 v4.25.3 // system info metrics
|
github.com/shirou/gopsutil/v4 v4.25.3 // system info metrics
|
||||||
@@ -24,75 +26,229 @@ require (
|
|||||||
golang.org/x/oauth2 v0.29.0 // oauth2 authentication
|
golang.org/x/oauth2 v0.29.0 // oauth2 authentication
|
||||||
golang.org/x/text v0.24.0 // string utilities
|
golang.org/x/text v0.24.0 // string utilities
|
||||||
golang.org/x/time v0.11.0 // time utilities
|
golang.org/x/time v0.11.0 // time utilities
|
||||||
gopkg.in/yaml.v3 v3.0.1 // yaml parsing for different config files
|
gopkg.in/yaml.v3 v3.0.1 // indirect; yaml parsing for different config files
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/coreos/go-oidc/v3 => github.com/godoxy-app/go-oidc/v3 v3.14.1
|
replace github.com/coreos/go-oidc/v3 => github.com/godoxy-app/go-oidc/v3 v3.14.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/docker/cli v28.1.1+incompatible
|
github.com/docker/cli v28.1.1+incompatible
|
||||||
github.com/docker/go-connections v0.5.0
|
github.com/goccy/go-yaml v1.17.1
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
|
github.com/luthermonson/go-proxmox v0.2.2
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1
|
||||||
|
github.com/quic-go/quic-go v0.51.0
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.7.3
|
||||||
|
github.com/spf13/afero v1.14.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
|
github.com/yusing/go-proxy/agent v0.0.0-20250428032249-8da63daf0202
|
||||||
|
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-20250428032249-8da63daf0202
|
||||||
|
go.uber.org/atomic v1.11.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/auth v0.16.1 // indirect
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||||
|
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||||
|
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
||||||
|
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.2 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.51.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||||
|
github.com/aws/smithy-go v1.22.3 // indirect
|
||||||
|
github.com/baidubce/bce-sdk-go v0.9.224 // indirect
|
||||||
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
|
github.com/boombuler/barcode v1.0.2 // indirect
|
||||||
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/civo/civogo v0.3.99 // indirect
|
||||||
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
|
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/diskfs/go-diskfs v1.6.0 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/djherbis/times v1.6.0 // indirect
|
||||||
|
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||||
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/ebitengine/purego v0.8.2 // indirect
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
|
github.com/exoscale/egoscale/v3 v3.1.15 // indirect
|
||||||
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
|
github.com/go-errors/errors v1.5.1 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect; indirectindirect
|
||||||
|
github.com/gofrs/flock v0.12.1 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||||
|
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
||||||
|
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||||
|
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||||
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.147 // indirect
|
||||||
|
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||||
|
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
|
||||||
|
github.com/jinzhu/copier v0.4.0 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
||||||
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||||
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
|
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||||
|
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/linode/linodego v1.49.0 // indirect
|
||||||
|
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
|
||||||
|
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||||
|
github.com/magefile/mage v1.15.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/miekg/dns v1.1.65 // indirect
|
github.com/miekg/dns v1.1.65 // indirect
|
||||||
|
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/moby/term v0.5.0 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||||
|
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
|
||||||
|
github.com/nrdcg/desec v0.11.0 // indirect
|
||||||
|
github.com/nrdcg/freemyip v0.3.0 // indirect
|
||||||
|
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||||
|
github.com/nrdcg/goinwx v0.11.0 // indirect
|
||||||
|
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||||
|
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||||
|
github.com/nrdcg/nodion v0.1.0 // indirect
|
||||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||||
|
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
|
github.com/oracle/oci-go-sdk/v65 v65.89.2 // indirect
|
||||||
github.com/ovh/go-ovh v1.7.0 // indirect
|
github.com/ovh/go-ovh v1.7.0 // indirect
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/peterhellberg/link v1.2.0 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/pquerna/otp v1.4.0 // indirect
|
||||||
github.com/prometheus/common v0.63.0 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/prometheus/procfs v0.16.1 // indirect
|
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
github.com/sacloud/api-client-go v0.2.10 // indirect
|
||||||
|
github.com/sacloud/go-http v0.1.9 // indirect
|
||||||
|
github.com/sacloud/iaas-api-go v1.14.0 // indirect
|
||||||
|
github.com/sacloud/packages-go v0.0.11 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
|
github.com/samber/lo v1.50.0 // indirect
|
||||||
|
github.com/samber/slog-common v0.18.1 // indirect
|
||||||
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 // indirect
|
||||||
|
github.com/selectel/domains-go v1.1.0 // indirect
|
||||||
|
github.com/selectel/go-selvpcclient/v3 v3.2.1 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||||
|
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||||
|
github.com/softlayer/softlayer-go v1.1.7 // indirect
|
||||||
|
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||||
|
github.com/sony/gobreaker v1.0.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/cast v1.7.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
|
github.com/spf13/viper v1.20.1 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1154 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 // indirect
|
||||||
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
|
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||||
|
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
||||||
|
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||||
|
github.com/volcengine/volc-sdk-golang v1.0.206 // indirect
|
||||||
|
github.com/vultr/govultr/v3 v3.19.1 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||||
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.1 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/ratelimit v0.3.1 // indirect
|
||||||
golang.org/x/mod v0.24.0 // indirect
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
golang.org/x/sync v0.13.0 // indirect
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.32.0 // indirect
|
golang.org/x/tools v0.32.0 // indirect
|
||||||
|
google.golang.org/api v0.230.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197 // indirect
|
||||||
|
google.golang.org/grpc v1.72.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gotest.tools/v3 v3.5.1 // indirect
|
gopkg.in/ns1/ns1-go.v2 v2.14.2 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
k8s.io/api v0.33.0 // indirect
|
||||||
|
k8s.io/apimachinery v0.33.0 // indirect
|
||||||
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
|
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
|
||||||
|
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||||
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
37
internal/acl/city_cache.go
Normal file
37
internal/acl/city_cache.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
|
acl "github.com/yusing/go-proxy/internal/acl/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cityCache = xsync.NewMapOf[string, *acl.City]()
|
||||||
|
|
||||||
|
func (cfg *MaxMindConfig) lookupCity(ip *acl.IPInfo) (*acl.City, bool) {
|
||||||
|
if ip.City != nil {
|
||||||
|
return ip.City, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.db.Reader == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
city, ok := cityCache.Load(ip.Str)
|
||||||
|
if ok {
|
||||||
|
ip.City = city
|
||||||
|
return city, true
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.db.RLock()
|
||||||
|
defer cfg.db.RUnlock()
|
||||||
|
|
||||||
|
city = new(acl.City)
|
||||||
|
err := cfg.db.Lookup(ip.IP, city)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
cityCache.Store(ip.Str, city)
|
||||||
|
ip.City = city
|
||||||
|
return city, true
|
||||||
|
}
|
||||||
219
internal/acl/config.go
Normal file
219
internal/acl/config.go
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oschwald/maxminddb-golang"
|
||||||
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
acl "github.com/yusing/go-proxy/internal/acl/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging/accesslog"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Default string `json:"default" validate:"omitempty,oneof=allow deny"` // default: allow
|
||||||
|
AllowLocal *bool `json:"allow_local"` // default: true
|
||||||
|
Allow []string `json:"allow"`
|
||||||
|
Deny []string `json:"deny"`
|
||||||
|
Log *accesslog.ACLLoggerConfig `json:"log"`
|
||||||
|
|
||||||
|
MaxMind *MaxMindConfig `json:"maxmind" validate:"omitempty"`
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
MaxMindDatabaseType string
|
||||||
|
MaxMindConfig struct {
|
||||||
|
AccountID string `json:"account_id" validate:"required"`
|
||||||
|
LicenseKey string `json:"license_key" validate:"required"`
|
||||||
|
Database MaxMindDatabaseType `json:"database" validate:"required,oneof=geolite geoip2"`
|
||||||
|
|
||||||
|
logger zerolog.Logger
|
||||||
|
lastUpdate time.Time
|
||||||
|
db struct {
|
||||||
|
*maxminddb.Reader
|
||||||
|
sync.RWMutex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
defaultAllow bool
|
||||||
|
allowLocal bool
|
||||||
|
allow []matcher
|
||||||
|
deny []matcher
|
||||||
|
ipCache *xsync.MapOf[string, *checkCache]
|
||||||
|
logAllowed bool
|
||||||
|
logger *accesslog.AccessLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
type checkCache struct {
|
||||||
|
*acl.IPInfo
|
||||||
|
allow bool
|
||||||
|
created time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheTTL = 1 * time.Minute
|
||||||
|
|
||||||
|
func (c *checkCache) Expired() bool {
|
||||||
|
return c.created.Add(cacheTTL).Before(utils.TimeNow())
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: add stats
|
||||||
|
|
||||||
|
const (
|
||||||
|
ACLAllow = "allow"
|
||||||
|
ACLDeny = "deny"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MaxMindGeoLite MaxMindDatabaseType = "geolite"
|
||||||
|
MaxMindGeoIP2 MaxMindDatabaseType = "geoip2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Config) Validate() gperr.Error {
|
||||||
|
switch c.Default {
|
||||||
|
case "", ACLAllow:
|
||||||
|
c.defaultAllow = true
|
||||||
|
case ACLDeny:
|
||||||
|
c.defaultAllow = false
|
||||||
|
default:
|
||||||
|
return gperr.New("invalid default value").Subject(c.Default)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.AllowLocal != nil {
|
||||||
|
c.allowLocal = *c.AllowLocal
|
||||||
|
} else {
|
||||||
|
c.allowLocal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.MaxMind != nil {
|
||||||
|
c.MaxMind.logger = logging.With().Str("type", string(c.MaxMind.Database)).Logger()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Log != nil {
|
||||||
|
c.logAllowed = c.Log.LogAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := gperr.NewBuilder("syntax error")
|
||||||
|
c.allow = make([]matcher, 0, len(c.Allow))
|
||||||
|
c.deny = make([]matcher, 0, len(c.Deny))
|
||||||
|
|
||||||
|
for _, s := range c.Allow {
|
||||||
|
m, err := c.parseMatcher(s)
|
||||||
|
if err != nil {
|
||||||
|
errs.Add(err.Subject(s))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.allow = append(c.allow, m)
|
||||||
|
}
|
||||||
|
for _, s := range c.Deny {
|
||||||
|
m, err := c.parseMatcher(s)
|
||||||
|
if err != nil {
|
||||||
|
errs.Add(err.Subject(s))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.deny = append(c.deny, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs.HasError() {
|
||||||
|
c.allow = nil
|
||||||
|
c.deny = nil
|
||||||
|
return errMatcherFormat.With(errs.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ipCache = xsync.NewMapOf[string, *checkCache]()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Valid() bool {
|
||||||
|
return c != nil && (len(c.allow) > 0 || len(c.deny) > 0 || c.allowLocal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Start(parent *task.Task) gperr.Error {
|
||||||
|
if c.MaxMind != nil {
|
||||||
|
if err := c.MaxMind.LoadMaxMindDB(parent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.Log != nil {
|
||||||
|
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
||||||
|
if err != nil {
|
||||||
|
return gperr.New("failed to start access logger").With(err)
|
||||||
|
}
|
||||||
|
c.logger = logger
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) cacheRecord(info *acl.IPInfo, allow bool) {
|
||||||
|
if common.ForceResolveCountry && info.City == nil {
|
||||||
|
c.MaxMind.lookupCity(info)
|
||||||
|
}
|
||||||
|
c.ipCache.Store(info.Str, &checkCache{
|
||||||
|
IPInfo: info,
|
||||||
|
allow: allow,
|
||||||
|
created: utils.TimeNow(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *config) log(info *acl.IPInfo, allowed bool) {
|
||||||
|
if c.logger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !allowed || c.logAllowed {
|
||||||
|
c.logger.LogACL(info, !allowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) IPAllowed(ip net.IP) bool {
|
||||||
|
if ip == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// always allow loopback
|
||||||
|
// loopback is not logged
|
||||||
|
if ip.IsLoopback() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.allowLocal && ip.IsPrivate() {
|
||||||
|
c.log(&acl.IPInfo{IP: ip, Str: ip.String()}, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ipStr := ip.String()
|
||||||
|
record, ok := c.ipCache.Load(ipStr)
|
||||||
|
if ok && !record.Expired() {
|
||||||
|
c.log(record.IPInfo, record.allow)
|
||||||
|
return record.allow
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAndStr := &acl.IPInfo{IP: ip, Str: ipStr}
|
||||||
|
for _, m := range c.allow {
|
||||||
|
if m(ipAndStr) {
|
||||||
|
c.log(ipAndStr, true)
|
||||||
|
c.cacheRecord(ipAndStr, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, m := range c.deny {
|
||||||
|
if m(ipAndStr) {
|
||||||
|
c.log(ipAndStr, false)
|
||||||
|
c.cacheRecord(ipAndStr, false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.log(ipAndStr, c.defaultAllow)
|
||||||
|
c.cacheRecord(ipAndStr, c.defaultAllow)
|
||||||
|
return c.defaultAllow
|
||||||
|
}
|
||||||
99
internal/acl/matcher.go
Normal file
99
internal/acl/matcher.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
acl "github.com/yusing/go-proxy/internal/acl/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type matcher func(*acl.IPInfo) bool
|
||||||
|
|
||||||
|
const (
|
||||||
|
MatcherTypeIP = "ip"
|
||||||
|
MatcherTypeCIDR = "cidr"
|
||||||
|
MatcherTypeTimeZone = "tz"
|
||||||
|
MatcherTypeCountry = "country"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errMatcherFormat = gperr.Multiline().AddLines(
|
||||||
|
"invalid matcher format, expect {type}:{value}",
|
||||||
|
"Available types: ip|cidr|tz|country",
|
||||||
|
"ip:127.0.0.1",
|
||||||
|
"cidr:127.0.0.0/8",
|
||||||
|
"tz:Asia/Shanghai",
|
||||||
|
"country:GB",
|
||||||
|
)
|
||||||
|
var (
|
||||||
|
errSyntax = gperr.New("syntax error")
|
||||||
|
errInvalidIP = gperr.New("invalid IP")
|
||||||
|
errInvalidCIDR = gperr.New("invalid CIDR")
|
||||||
|
errMaxMindNotConfigured = gperr.New("MaxMind not configured")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (cfg *Config) parseMatcher(s string) (matcher, gperr.Error) {
|
||||||
|
parts := strings.Split(s, ":")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, errSyntax
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parts[0] {
|
||||||
|
case MatcherTypeIP:
|
||||||
|
ip := net.ParseIP(parts[1])
|
||||||
|
if ip == nil {
|
||||||
|
return nil, errInvalidIP
|
||||||
|
}
|
||||||
|
return matchIP(ip), nil
|
||||||
|
case MatcherTypeCIDR:
|
||||||
|
_, net, err := net.ParseCIDR(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, errInvalidCIDR
|
||||||
|
}
|
||||||
|
return matchCIDR(net), nil
|
||||||
|
case MatcherTypeTimeZone:
|
||||||
|
if cfg.MaxMind == nil {
|
||||||
|
return nil, errMaxMindNotConfigured
|
||||||
|
}
|
||||||
|
return cfg.MaxMind.matchTimeZone(parts[1]), nil
|
||||||
|
case MatcherTypeCountry:
|
||||||
|
if cfg.MaxMind == nil {
|
||||||
|
return nil, errMaxMindNotConfigured
|
||||||
|
}
|
||||||
|
return cfg.MaxMind.matchISOCode(parts[1]), nil
|
||||||
|
default:
|
||||||
|
return nil, errSyntax
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchIP(ip net.IP) matcher {
|
||||||
|
return func(ip2 *acl.IPInfo) bool {
|
||||||
|
return ip.Equal(ip2.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchCIDR(n *net.IPNet) matcher {
|
||||||
|
return func(ip *acl.IPInfo) bool {
|
||||||
|
return n.Contains(ip.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *MaxMindConfig) matchTimeZone(tz string) matcher {
|
||||||
|
return func(ip *acl.IPInfo) bool {
|
||||||
|
city, ok := cfg.lookupCity(ip)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return city.Location.TimeZone == tz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *MaxMindConfig) matchISOCode(iso string) matcher {
|
||||||
|
return func(ip *acl.IPInfo) bool {
|
||||||
|
city, ok := cfg.lookupCity(ip)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return city.Country.IsoCode == iso
|
||||||
|
}
|
||||||
|
}
|
||||||
303
internal/acl/maxmind.go
Normal file
303
internal/acl/maxmind.go
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oschwald/maxminddb-golang"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
updateInterval = 24 * time.Hour
|
||||||
|
httpClient = &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
ErrResponseNotOK = gperr.New("response not OK")
|
||||||
|
ErrDownloadFailure = gperr.New("download failure")
|
||||||
|
)
|
||||||
|
|
||||||
|
func dbPathImpl(dbType MaxMindDatabaseType) string {
|
||||||
|
if dbType == MaxMindGeoLite {
|
||||||
|
return filepath.Join(dataDir, "GeoLite2-City.mmdb")
|
||||||
|
}
|
||||||
|
return filepath.Join(dataDir, "GeoIP2-City.mmdb")
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbURLimpl(dbType MaxMindDatabaseType) string {
|
||||||
|
if dbType == MaxMindGeoLite {
|
||||||
|
return "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"
|
||||||
|
}
|
||||||
|
return "https://download.maxmind.com/geoip/databases/GeoIP2-City/download?suffix=tar.gz"
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbFilename(dbType MaxMindDatabaseType) string {
|
||||||
|
if dbType == MaxMindGeoLite {
|
||||||
|
return "GeoLite2-City.mmdb"
|
||||||
|
}
|
||||||
|
return "GeoIP2-City.mmdb"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *MaxMindConfig) LoadMaxMindDB(parent task.Parent) gperr.Error {
|
||||||
|
if cfg.Database == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
path := dbPath(cfg.Database)
|
||||||
|
reader, err := maxmindDBOpen(path)
|
||||||
|
valid := true
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, os.ErrNotExist):
|
||||||
|
default:
|
||||||
|
// ignore invalid error, just download it again
|
||||||
|
var invalidErr maxminddb.InvalidDatabaseError
|
||||||
|
if !errors.As(err, &invalidErr) {
|
||||||
|
return gperr.Wrap(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
cfg.logger.Info().Msg("MaxMind DB not found/invalid, downloading...")
|
||||||
|
if err = cfg.download(); err != nil {
|
||||||
|
return ErrDownloadFailure.With(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cfg.logger.Info().Msg("MaxMind DB loaded")
|
||||||
|
cfg.db.Reader = reader
|
||||||
|
go cfg.scheduleUpdate(parent)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *MaxMindConfig) loadLastUpdate() {
|
||||||
|
f, err := os.Stat(dbPath(cfg.Database))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg.lastUpdate = f.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *MaxMindConfig) setLastUpdate(t time.Time) {
|
||||||
|
cfg.lastUpdate = t
|
||||||
|
_ = os.Chtimes(dbPath(cfg.Database), t, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *MaxMindConfig) scheduleUpdate(parent task.Parent) {
|
||||||
|
task := parent.Subtask("schedule_update", true)
|
||||||
|
ticker := time.NewTicker(updateInterval)
|
||||||
|
|
||||||
|
cfg.loadLastUpdate()
|
||||||
|
cfg.update()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
ticker.Stop()
|
||||||
|
if cfg.db.Reader != nil {
|
||||||
|
cfg.db.Reader.Close()
|
||||||
|
}
|
||||||
|
task.Finish(nil)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-task.Context().Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
cfg.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *MaxMindConfig) update() {
|
||||||
|
// check for update
|
||||||
|
cfg.logger.Info().Msg("checking for MaxMind DB update...")
|
||||||
|
remoteLastModified, err := cfg.checkLastest()
|
||||||
|
if err != nil {
|
||||||
|
cfg.logger.Err(err).Msg("failed to check MaxMind DB update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if remoteLastModified.Equal(cfg.lastUpdate) {
|
||||||
|
cfg.logger.Info().Msg("MaxMind DB is up to date")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.logger.Info().
|
||||||
|
Time("latest", remoteLastModified.Local()).
|
||||||
|
Time("current", cfg.lastUpdate).
|
||||||
|
Msg("MaxMind DB update available")
|
||||||
|
if err = cfg.download(); err != nil {
|
||||||
|
cfg.logger.Err(err).Msg("failed to update MaxMind DB")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg.logger.Info().Msg("MaxMind DB updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *MaxMindConfig) newReq(method string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(method, dbURL(cfg.Database), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.SetBasicAuth(cfg.AccountID, cfg.LicenseKey)
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *MaxMindConfig) checkLastest() (lastModifiedT *time.Time, err error) {
|
||||||
|
resp, err := newReq(cfg, http.MethodHead)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastModified := resp.Header.Get("Last-Modified")
|
||||||
|
if lastModified == "" {
|
||||||
|
cfg.logger.Warn().Msg("MaxMind responded no last modified time, update skipped")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastModifiedTime, err := time.Parse(http.TimeFormat, lastModified)
|
||||||
|
if err != nil {
|
||||||
|
cfg.logger.Warn().Err(err).Msg("MaxMind responded invalid last modified time, update skipped")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &lastModifiedTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *MaxMindConfig) download() error {
|
||||||
|
resp, err := newReq(cfg, http.MethodGet)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbFile := dbPath(cfg.Database)
|
||||||
|
tmpGZPath := dbFile + "-tmp.tar.gz"
|
||||||
|
tmpDBPath := dbFile + "-tmp"
|
||||||
|
|
||||||
|
tmpGZFile, err := os.OpenFile(tmpGZPath, os.O_CREATE|os.O_RDWR, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup the tar.gz file
|
||||||
|
defer func() {
|
||||||
|
_ = tmpGZFile.Close()
|
||||||
|
_ = os.Remove(tmpGZPath)
|
||||||
|
}()
|
||||||
|
|
||||||
|
cfg.logger.Info().Msg("MaxMind DB downloading...")
|
||||||
|
|
||||||
|
_, err = io.Copy(tmpGZFile, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tmpGZFile.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract .tar.gz and to database
|
||||||
|
err = extractFileFromTarGz(tmpGZFile, dbFilename(cfg.Database), tmpDBPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return gperr.New("failed to extract database from archive").With(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if the downloaded database is valid
|
||||||
|
db, err := maxmindDBOpen(tmpDBPath)
|
||||||
|
if err != nil {
|
||||||
|
_ = os.Remove(tmpDBPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Close()
|
||||||
|
err = os.Rename(tmpDBPath, dbFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.db.Lock()
|
||||||
|
defer cfg.db.Unlock()
|
||||||
|
if cfg.db.Reader != nil {
|
||||||
|
cfg.db.Reader.Close()
|
||||||
|
}
|
||||||
|
cfg.db.Reader, err = maxmindDBOpen(dbFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lastModifiedStr := resp.Header.Get("Last-Modified")
|
||||||
|
lastModifiedTime, err := time.Parse(http.TimeFormat, lastModifiedStr)
|
||||||
|
if err == nil {
|
||||||
|
cfg.setLastUpdate(lastModifiedTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.logger.Info().Msg("MaxMind DB downloaded")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFileFromTarGz(tarGzFile *os.File, targetFilename, destPath string) error {
|
||||||
|
defer tarGzFile.Close()
|
||||||
|
|
||||||
|
gzr, err := gzip.NewReader(tarGzFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer gzr.Close()
|
||||||
|
|
||||||
|
tr := tar.NewReader(gzr)
|
||||||
|
for {
|
||||||
|
hdr, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break // End of archive
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Only extract the file that matches targetFilename (basename match)
|
||||||
|
if filepath.Base(hdr.Name) == targetFilename {
|
||||||
|
outFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, hdr.FileInfo().Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
_, err = io.Copy(outFile, tr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil // Done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("file %s not found in archive", targetFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
dataDir = common.DataDir
|
||||||
|
dbURL = dbURLimpl
|
||||||
|
dbPath = dbPathImpl
|
||||||
|
maxmindDBOpen = maxminddb.Open
|
||||||
|
newReq = (*MaxMindConfig).newReq
|
||||||
|
)
|
||||||
223
internal/acl/maxmind_test.go
Normal file
223
internal/acl/maxmind_test.go
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oschwald/maxminddb-golang"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_dbPath(t *testing.T) {
|
||||||
|
tmpDataDir := "/tmp/testdata"
|
||||||
|
oldDataDir := dataDir
|
||||||
|
dataDir = tmpDataDir
|
||||||
|
defer func() { dataDir = oldDataDir }()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
dbType MaxMindDatabaseType
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"GeoLite", MaxMindGeoLite, filepath.Join(tmpDataDir, "GeoLite2-City.mmdb")},
|
||||||
|
{"GeoIP2", MaxMindGeoIP2, filepath.Join(tmpDataDir, "GeoIP2-City.mmdb")},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := dbPath(tt.dbType); got != tt.want {
|
||||||
|
t.Errorf("dbPath() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_dbURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
dbType MaxMindDatabaseType
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"GeoLite", MaxMindGeoLite, "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"},
|
||||||
|
{"GeoIP2", MaxMindGeoIP2, "https://download.maxmind.com/geoip/databases/GeoIP2-City/download?suffix=tar.gz"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := dbURL(tt.dbType); got != tt.want {
|
||||||
|
t.Errorf("dbURL() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper for MaxMindConfig ---
|
||||||
|
type testLogger struct{ zerolog.Logger }
|
||||||
|
|
||||||
|
func (testLogger) Info() *zerolog.Event { return &zerolog.Event{} }
|
||||||
|
func (testLogger) Warn() *zerolog.Event { return &zerolog.Event{} }
|
||||||
|
func (testLogger) Err(_ error) *zerolog.Event { return &zerolog.Event{} }
|
||||||
|
|
||||||
|
func Test_MaxMindConfig_newReq(t *testing.T) {
|
||||||
|
cfg := &MaxMindConfig{
|
||||||
|
AccountID: "testid",
|
||||||
|
LicenseKey: "testkey",
|
||||||
|
Database: MaxMindGeoLite,
|
||||||
|
logger: zerolog.Nop(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch httpClient to use httptest
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if u, p, ok := r.BasicAuth(); !ok || u != "testid" || p != "testkey" {
|
||||||
|
t.Errorf("basic auth not set correctly")
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
oldURL := dbURL
|
||||||
|
dbURL = func(MaxMindDatabaseType) string { return server.URL }
|
||||||
|
defer func() { dbURL = oldURL }()
|
||||||
|
|
||||||
|
resp, err := cfg.newReq(http.MethodGet)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("newReq() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("unexpected status: %v", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_MaxMindConfig_checkUpdate(t *testing.T) {
|
||||||
|
cfg := &MaxMindConfig{
|
||||||
|
AccountID: "id",
|
||||||
|
LicenseKey: "key",
|
||||||
|
Database: MaxMindGeoLite,
|
||||||
|
logger: zerolog.Nop(),
|
||||||
|
}
|
||||||
|
lastMod := time.Now().UTC().Format(http.TimeFormat)
|
||||||
|
buildTime := time.Now().Add(-time.Hour)
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Last-Modified", lastMod)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
oldURL := dbURL
|
||||||
|
dbURL = func(MaxMindDatabaseType) string { return server.URL }
|
||||||
|
defer func() { dbURL = oldURL }()
|
||||||
|
|
||||||
|
latest, err := cfg.checkLastest()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("checkUpdate() error = %v", err)
|
||||||
|
}
|
||||||
|
if latest.Equal(buildTime) {
|
||||||
|
t.Errorf("expected update needed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeReadCloser struct {
|
||||||
|
firstRead bool
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeReadCloser) Read(p []byte) (int, error) {
|
||||||
|
if !c.firstRead {
|
||||||
|
c.firstRead = true
|
||||||
|
return strings.NewReader("FAKEMMDB").Read(p)
|
||||||
|
}
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeReadCloser) Close() error {
|
||||||
|
c.closed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_MaxMindConfig_download(t *testing.T) {
|
||||||
|
cfg := &MaxMindConfig{
|
||||||
|
AccountID: "id",
|
||||||
|
LicenseKey: "key",
|
||||||
|
Database: MaxMindGeoLite,
|
||||||
|
logger: zerolog.Nop(),
|
||||||
|
}
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gz := gzip.NewWriter(w)
|
||||||
|
t := tar.NewWriter(gz)
|
||||||
|
t.WriteHeader(&tar.Header{
|
||||||
|
Name: dbFilename(MaxMindGeoLite),
|
||||||
|
})
|
||||||
|
t.Write([]byte("1234"))
|
||||||
|
t.Close()
|
||||||
|
gz.Close()
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
oldURL := dbURL
|
||||||
|
dbURL = func(MaxMindDatabaseType) string { return server.URL }
|
||||||
|
defer func() { dbURL = oldURL }()
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
oldDataDir := dataDir
|
||||||
|
dataDir = tmpDir
|
||||||
|
defer func() { dataDir = oldDataDir }()
|
||||||
|
|
||||||
|
// Patch maxminddb.Open to always succeed
|
||||||
|
origOpen := maxmindDBOpen
|
||||||
|
maxmindDBOpen = func(path string) (*maxminddb.Reader, error) {
|
||||||
|
return &maxminddb.Reader{}, nil
|
||||||
|
}
|
||||||
|
defer func() { maxmindDBOpen = origOpen }()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("newReq() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
oldNewReq := newReq
|
||||||
|
newReq = func(cfg *MaxMindConfig, method string) (*http.Response, error) {
|
||||||
|
server.Config.Handler.ServeHTTP(rw, req)
|
||||||
|
return rw.Result(), nil
|
||||||
|
}
|
||||||
|
defer func() { newReq = oldNewReq }()
|
||||||
|
|
||||||
|
err = cfg.download()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("download() error = %v", err)
|
||||||
|
}
|
||||||
|
if cfg.db.Reader == nil {
|
||||||
|
t.Error("expected db instance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_MaxMindConfig_loadMaxMindDB(t *testing.T) {
|
||||||
|
// This test should cover both the path where DB exists and where it does not
|
||||||
|
// For brevity, only the non-existing path is tested here
|
||||||
|
cfg := &MaxMindConfig{
|
||||||
|
AccountID: "id",
|
||||||
|
LicenseKey: "key",
|
||||||
|
Database: MaxMindGeoLite,
|
||||||
|
logger: zerolog.Nop(),
|
||||||
|
}
|
||||||
|
oldOpen := maxmindDBOpen
|
||||||
|
maxmindDBOpen = func(path string) (*maxminddb.Reader, error) {
|
||||||
|
return &maxminddb.Reader{}, nil
|
||||||
|
}
|
||||||
|
defer func() { maxmindDBOpen = oldOpen }()
|
||||||
|
|
||||||
|
oldDBPath := dbPath
|
||||||
|
dbPath = func(MaxMindDatabaseType) string { return filepath.Join(t.TempDir(), "maxmind.mmdb") }
|
||||||
|
defer func() { dbPath = oldDBPath }()
|
||||||
|
|
||||||
|
task := task.RootTask("test")
|
||||||
|
defer task.Finish(nil)
|
||||||
|
err := cfg.LoadMaxMindDB(task)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("loadMaxMindDB() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
internal/acl/tcp_listener.go
Normal file
59
internal/acl/tcp_listener.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TCPListener struct {
|
||||||
|
acl *Config
|
||||||
|
lis net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
type noConn struct{}
|
||||||
|
|
||||||
|
func (noConn) Read(b []byte) (int, error) { return 0, io.EOF }
|
||||||
|
func (noConn) Write(b []byte) (int, error) { return 0, io.EOF }
|
||||||
|
func (noConn) Close() error { return nil }
|
||||||
|
func (noConn) LocalAddr() net.Addr { return nil }
|
||||||
|
func (noConn) RemoteAddr() net.Addr { return nil }
|
||||||
|
func (noConn) SetDeadline(t time.Time) error { return nil }
|
||||||
|
func (noConn) SetReadDeadline(t time.Time) error { return nil }
|
||||||
|
func (noConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||||
|
|
||||||
|
func (cfg *Config) WrapTCP(lis net.Listener) net.Listener {
|
||||||
|
if cfg == nil {
|
||||||
|
return lis
|
||||||
|
}
|
||||||
|
return &TCPListener{
|
||||||
|
acl: cfg,
|
||||||
|
lis: lis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPListener) Addr() net.Addr {
|
||||||
|
return s.lis.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPListener) Accept() (net.Conn, error) {
|
||||||
|
c, err := s.lis.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
addr, ok := c.RemoteAddr().(*net.TCPAddr)
|
||||||
|
if !ok {
|
||||||
|
// Not a TCPAddr, drop
|
||||||
|
c.Close()
|
||||||
|
return noConn{}, nil
|
||||||
|
}
|
||||||
|
if !s.acl.IPAllowed(addr.IP) {
|
||||||
|
c.Close()
|
||||||
|
return noConn{}, nil
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPListener) Close() error {
|
||||||
|
return s.lis.Close()
|
||||||
|
}
|
||||||
10
internal/acl/types/city_info.go
Normal file
10
internal/acl/types/city_info.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
type City struct {
|
||||||
|
Location struct {
|
||||||
|
TimeZone string `maxminddb:"time_zone"`
|
||||||
|
} `maxminddb:"location"`
|
||||||
|
Country struct {
|
||||||
|
IsoCode string `maxminddb:"iso_code"`
|
||||||
|
} `maxminddb:"country"`
|
||||||
|
}
|
||||||
9
internal/acl/types/ip_info.go
Normal file
9
internal/acl/types/ip_info.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
type IPInfo struct {
|
||||||
|
IP net.IP
|
||||||
|
Str string
|
||||||
|
City *City
|
||||||
|
}
|
||||||
79
internal/acl/udp_listener.go
Normal file
79
internal/acl/udp_listener.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UDPListener struct {
|
||||||
|
acl *Config
|
||||||
|
lis net.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) WrapUDP(lis net.PacketConn) net.PacketConn {
|
||||||
|
if cfg == nil {
|
||||||
|
return lis
|
||||||
|
}
|
||||||
|
return &UDPListener{
|
||||||
|
acl: cfg,
|
||||||
|
lis: lis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) LocalAddr() net.Addr {
|
||||||
|
return s.lis.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||||
|
for {
|
||||||
|
n, addr, err := s.lis.ReadFrom(p)
|
||||||
|
if err != nil {
|
||||||
|
return n, addr, err
|
||||||
|
}
|
||||||
|
udpAddr, ok := addr.(*net.UDPAddr)
|
||||||
|
if !ok {
|
||||||
|
// Not a UDPAddr, drop
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !s.acl.IPAllowed(udpAddr.IP) {
|
||||||
|
// Drop packet from disallowed IP
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return n, addr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||||
|
for {
|
||||||
|
n, err := s.lis.WriteTo(p, addr)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
udpAddr, ok := addr.(*net.UDPAddr)
|
||||||
|
if !ok {
|
||||||
|
// Not a UDPAddr, drop
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !s.acl.IPAllowed(udpAddr.IP) {
|
||||||
|
// Drop packet to disallowed IP
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) SetDeadline(t time.Time) error {
|
||||||
|
return s.lis.SetDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) SetReadDeadline(t time.Time) error {
|
||||||
|
return s.lis.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) SetWriteDeadline(t time.Time) error {
|
||||||
|
return s.lis.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) Close() error {
|
||||||
|
return s.lis.Close()
|
||||||
|
}
|
||||||
@@ -4,19 +4,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/certapi"
|
"github.com/yusing/go-proxy/internal/api/v1/certapi"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/dockerapi"
|
"github.com/yusing/go-proxy/internal/api/v1/dockerapi"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/auth"
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
|
||||||
"github.com/yusing/go-proxy/internal/logging/memlogger"
|
"github.com/yusing/go-proxy/internal/logging/memlogger"
|
||||||
"github.com/yusing/go-proxy/internal/metrics/uptime"
|
"github.com/yusing/go-proxy/internal/metrics/uptime"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
"github.com/yusing/go-proxy/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -68,7 +66,7 @@ func (mux ServeMux) HandleFunc(methods, endpoint string, h any, requireAuth ...b
|
|||||||
func NewHandler(cfg config.ConfigInstance) http.Handler {
|
func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||||
mux := ServeMux{http.NewServeMux(), cfg}
|
mux := ServeMux{http.NewServeMux(), cfg}
|
||||||
mux.HandleFunc("GET", "/v1", v1.Index)
|
mux.HandleFunc("GET", "/v1", v1.Index)
|
||||||
mux.HandleFunc("GET", "/v1/version", v1.GetVersion)
|
mux.HandleFunc("GET", "/v1/version", pkg.GetVersionHTTPHandler())
|
||||||
|
|
||||||
mux.HandleFunc("GET", "/v1/stats", v1.Stats, true)
|
mux.HandleFunc("GET", "/v1/stats", v1.Stats, true)
|
||||||
mux.HandleFunc("POST", "/v1/reload", v1.Reload, true)
|
mux.HandleFunc("POST", "/v1/reload", v1.Reload, true)
|
||||||
@@ -93,26 +91,14 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
|
|||||||
mux.HandleFunc("GET", "/v1/docker/logs/{server}/{container}", dockerapi.Logs, true)
|
mux.HandleFunc("GET", "/v1/docker/logs/{server}/{container}", dockerapi.Logs, true)
|
||||||
mux.HandleFunc("GET", "/v1/docker/containers", dockerapi.Containers, true)
|
mux.HandleFunc("GET", "/v1/docker/containers", dockerapi.Containers, true)
|
||||||
|
|
||||||
if common.PrometheusEnabled {
|
defaultAuth := auth.GetDefaultAuth()
|
||||||
mux.Handle("GET /v1/metrics", promhttp.Handler())
|
if defaultAuth == nil {
|
||||||
logging.Info().Msg("prometheus metrics enabled")
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultAuth := auth.GetDefaultAuth()
|
mux.HandleFunc("GET", "/v1/auth/check", auth.AuthCheckHandler)
|
||||||
if defaultAuth != nil {
|
mux.HandleFunc("GET,POST", "/v1/auth/redirect", defaultAuth.LoginHandler)
|
||||||
mux.HandleFunc("GET", "/v1/auth/redirect", defaultAuth.RedirectLoginPage)
|
mux.HandleFunc("GET,POST", "/v1/auth/callback", defaultAuth.PostAuthCallbackHandler)
|
||||||
mux.HandleFunc("GET", "/v1/auth/check", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("GET,POST", "/v1/auth/logout", defaultAuth.LogoutHandler)
|
||||||
if err := defaultAuth.CheckToken(r); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
mux.HandleFunc("GET,POST", "/v1/auth/callback", defaultAuth.LoginCallbackHandler)
|
|
||||||
mux.HandleFunc("GET,POST", "/v1/auth/logout", defaultAuth.LogoutCallbackHandler)
|
|
||||||
} else {
|
|
||||||
mux.HandleFunc("GET", "/v1/auth/check", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,274 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
OIDCProvider struct {
|
|
||||||
oauthConfig *oauth2.Config
|
|
||||||
oidcProvider *oidc.Provider
|
|
||||||
oidcVerifier *oidc.IDTokenVerifier
|
|
||||||
oidcEndSessionURL *url.URL
|
|
||||||
allowedUsers []string
|
|
||||||
allowedGroups []string
|
|
||||||
isMiddleware bool
|
|
||||||
}
|
|
||||||
|
|
||||||
providerJSON struct {
|
|
||||||
oidc.ProviderConfig
|
|
||||||
EndSessionURL string `json:"end_session_endpoint"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const CookieOauthState = "godoxy_oidc_state"
|
|
||||||
|
|
||||||
const (
|
|
||||||
OIDCMiddlewareCallbackPath = "/auth/callback"
|
|
||||||
OIDCLogoutPath = "/auth/logout"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewOIDCProvider(issuerURL, clientID, clientSecret, redirectURL string, allowedUsers, allowedGroups []string) (*OIDCProvider, error) {
|
|
||||||
if len(allowedUsers)+len(allowedGroups) == 0 {
|
|
||||||
return nil, errors.New("OIDC users, groups, or both must not be empty")
|
|
||||||
}
|
|
||||||
provider, err := oidc.NewProvider(context.Background(), issuerURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
endSessionURL, err := url.Parse(provider.EndSessionEndpoint())
|
|
||||||
if err != nil && provider.EndSessionEndpoint() != "" {
|
|
||||||
// non critical, just warn
|
|
||||||
logging.Warn().
|
|
||||||
Str("issuer", issuerURL).
|
|
||||||
Err(err).
|
|
||||||
Msg("failed to parse end session URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &OIDCProvider{
|
|
||||||
oauthConfig: &oauth2.Config{
|
|
||||||
ClientID: clientID,
|
|
||||||
ClientSecret: clientSecret,
|
|
||||||
RedirectURL: redirectURL,
|
|
||||||
Endpoint: provider.Endpoint(),
|
|
||||||
Scopes: strutils.CommaSeperatedList(common.OIDCScopes),
|
|
||||||
},
|
|
||||||
oidcProvider: provider,
|
|
||||||
oidcVerifier: provider.Verifier(&oidc.Config{
|
|
||||||
ClientID: clientID,
|
|
||||||
}),
|
|
||||||
oidcEndSessionURL: endSessionURL,
|
|
||||||
allowedUsers: allowedUsers,
|
|
||||||
allowedGroups: allowedGroups,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOIDCProviderFromEnv creates a new OIDCProvider from environment variables.
|
|
||||||
func NewOIDCProviderFromEnv() (*OIDCProvider, error) {
|
|
||||||
return NewOIDCProvider(
|
|
||||||
common.OIDCIssuerURL,
|
|
||||||
common.OIDCClientID,
|
|
||||||
common.OIDCClientSecret,
|
|
||||||
common.OIDCRedirectURL,
|
|
||||||
common.OIDCAllowedUsers,
|
|
||||||
common.OIDCAllowedGroups,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) TokenCookieName() string {
|
|
||||||
return "godoxy_oidc_token"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) SetIsMiddleware(enabled bool) {
|
|
||||||
auth.isMiddleware = enabled
|
|
||||||
auth.oauthConfig.RedirectURL = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) SetAllowedUsers(users []string) {
|
|
||||||
auth.allowedUsers = users
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) SetAllowedGroups(groups []string) {
|
|
||||||
auth.allowedGroups = groups
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) CheckToken(r *http.Request) error {
|
|
||||||
token, err := r.Cookie(auth.TokenCookieName())
|
|
||||||
if err != nil {
|
|
||||||
return ErrMissingToken
|
|
||||||
}
|
|
||||||
|
|
||||||
// checks for Expiry, Audience == ClientID, Issuer, etc.
|
|
||||||
idToken, err := auth.oidcVerifier.Verify(r.Context(), token.Value)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to verify ID token: %w: %w", ErrInvalidToken, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(idToken.Audience) == 0 {
|
|
||||||
return ErrInvalidToken
|
|
||||||
}
|
|
||||||
|
|
||||||
var claims struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
Username string `json:"preferred_username"`
|
|
||||||
Groups []string `json:"groups"`
|
|
||||||
}
|
|
||||||
if err := idToken.Claims(&claims); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse claims: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logical AND between allowed users and groups.
|
|
||||||
allowedUser := slices.Contains(auth.allowedUsers, claims.Username)
|
|
||||||
allowedGroup := len(utils.Intersect(claims.Groups, auth.allowedGroups)) > 0
|
|
||||||
if !allowedUser && !allowedGroup {
|
|
||||||
return ErrUserNotAllowed
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateState generates a random string for OIDC state.
|
|
||||||
const oidcStateLength = 32
|
|
||||||
|
|
||||||
func generateState() (string, error) {
|
|
||||||
b := make([]byte, oidcStateLength)
|
|
||||||
_, err := rand.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.URLEncoding.EncodeToString(b)[:oidcStateLength], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedirectOIDC initiates the OIDC login flow.
|
|
||||||
func (auth *OIDCProvider) RedirectLoginPage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
state, err := generateState()
|
|
||||||
if err != nil {
|
|
||||||
gphttp.ServerError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: CookieOauthState,
|
|
||||||
Value: state,
|
|
||||||
MaxAge: 300,
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
Secure: common.APIJWTSecure,
|
|
||||||
Path: "/",
|
|
||||||
})
|
|
||||||
|
|
||||||
var redirURL string
|
|
||||||
if auth.isMiddleware {
|
|
||||||
optOverrideRedirectURL := oauth2.SetAuthURLParam("redirect_uri", "https://"+r.Host+OIDCMiddlewareCallbackPath)
|
|
||||||
redirURL = auth.oauthConfig.AuthCodeURL(state, optOverrideRedirectURL)
|
|
||||||
} else {
|
|
||||||
redirURL = auth.oauthConfig.AuthCodeURL(state)
|
|
||||||
}
|
|
||||||
http.Redirect(w, r, redirURL, http.StatusTemporaryRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) exchange(r *http.Request) (*oauth2.Token, error) {
|
|
||||||
if auth.isMiddleware {
|
|
||||||
cfg := *auth.oauthConfig
|
|
||||||
cfg.RedirectURL = "https://" + r.Host + OIDCMiddlewareCallbackPath
|
|
||||||
return cfg.Exchange(r.Context(), r.URL.Query().Get("code"))
|
|
||||||
}
|
|
||||||
return auth.oauthConfig.Exchange(r.Context(), r.URL.Query().Get("code"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// OIDCCallbackHandler handles the OIDC callback.
|
|
||||||
func (auth *OIDCProvider) LoginCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// For testing purposes, skip provider verification
|
|
||||||
if common.IsTest {
|
|
||||||
auth.handleTestCallback(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := r.Cookie(CookieOauthState)
|
|
||||||
if err != nil {
|
|
||||||
gphttp.BadRequest(w, "missing state cookie")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := r.URL.Query()
|
|
||||||
if query.Get("state") != state.Value {
|
|
||||||
gphttp.BadRequest(w, "invalid oauth state")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
oauth2Token, err := auth.exchange(r)
|
|
||||||
if err != nil {
|
|
||||||
gphttp.ServerError(w, r, fmt.Errorf("failed to exchange token: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
|
||||||
if !ok {
|
|
||||||
gphttp.BadRequest(w, "missing id_token")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
idToken, err := auth.oidcVerifier.Verify(r.Context(), rawIDToken)
|
|
||||||
if err != nil {
|
|
||||||
gphttp.ServerError(w, r, fmt.Errorf("failed to verify ID token: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setTokenCookie(w, r, auth.TokenCookieName(), rawIDToken, time.Until(idToken.Expiry))
|
|
||||||
|
|
||||||
// Redirect to home page
|
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) LogoutCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if auth.oidcEndSessionURL == nil {
|
|
||||||
DefaultLogoutCallbackHandler(auth, w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := r.Cookie(auth.TokenCookieName())
|
|
||||||
if err != nil {
|
|
||||||
gphttp.BadRequest(w, "missing token cookie")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
clearTokenCookie(w, r, auth.TokenCookieName())
|
|
||||||
|
|
||||||
logoutURL := *auth.oidcEndSessionURL
|
|
||||||
logoutURL.Query().Add("id_token_hint", token.Value)
|
|
||||||
|
|
||||||
http.Redirect(w, r, logoutURL.String(), http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleTestCallback handles OIDC callback in test environment.
|
|
||||||
func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Request) {
|
|
||||||
state, err := r.Cookie(CookieOauthState)
|
|
||||||
if err != nil {
|
|
||||||
gphttp.BadRequest(w, "missing state cookie")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.URL.Query().Get("state") != state.Value {
|
|
||||||
gphttp.BadRequest(w, "invalid oauth state")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create test JWT token
|
|
||||||
setTokenCookie(w, r, auth.TokenCookieName(), "test", time.Hour)
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Provider interface {
|
|
||||||
TokenCookieName() string
|
|
||||||
CheckToken(r *http.Request) error
|
|
||||||
RedirectLoginPage(w http.ResponseWriter, r *http.Request)
|
|
||||||
LoginCallbackHandler(w http.ResponseWriter, r *http.Request)
|
|
||||||
LogoutCallbackHandler(w http.ResponseWriter, r *http.Request)
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,7 @@ func (d *dockerInfo) MarshalJSON() ([]byte, error) {
|
|||||||
},
|
},
|
||||||
"images": d.Images,
|
"images": d.Images,
|
||||||
"n_cpu": d.NCPU,
|
"n_cpu": d.NCPU,
|
||||||
"memory": strutils.FormatByteSizeWithUnit(d.MemTotal),
|
"memory": strutils.FormatByteSize(d.MemTotal),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package dockerapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
@@ -9,15 +10,14 @@ import (
|
|||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Logs(w http.ResponseWriter, r *http.Request) {
|
func Logs(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
server := r.PathValue("server")
|
server := r.PathValue("server")
|
||||||
containerID := r.PathValue("container")
|
containerID := r.PathValue("container")
|
||||||
stdout := strutils.ParseBool(query.Get("stdout"))
|
stdout, _ := strconv.ParseBool(query.Get("stdout"))
|
||||||
stderr := strutils.ParseBool(query.Get("stderr"))
|
stderr, _ := strconv.ParseBool(query.Get("stderr"))
|
||||||
since := query.Get("from")
|
since := query.Get("from")
|
||||||
until := query.Get("to")
|
until := query.Get("to")
|
||||||
levels := query.Get("levels") // TODO: implement levels
|
levels := query.Get("levels") // TODO: implement levels
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
package favicon
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
|
||||||
route "github.com/yusing/go-proxy/internal/route/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type cacheEntry struct {
|
|
||||||
Icon []byte `json:"icon"`
|
|
||||||
LastAccess time.Time `json:"lastAccess"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// cache key can be absolute url or route name.
|
|
||||||
var (
|
|
||||||
iconCache = make(map[string]*cacheEntry)
|
|
||||||
iconCacheMu sync.RWMutex
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
iconCacheTTL = 3 * 24 * time.Hour
|
|
||||||
cleanUpInterval = time.Hour
|
|
||||||
)
|
|
||||||
|
|
||||||
func InitIconCache() {
|
|
||||||
iconCacheMu.Lock()
|
|
||||||
defer iconCacheMu.Unlock()
|
|
||||||
|
|
||||||
err := utils.LoadJSONIfExist(common.IconCachePath, &iconCache)
|
|
||||||
if err != nil {
|
|
||||||
logging.Error().Err(err).Msg("failed to load icon cache")
|
|
||||||
} else if len(iconCache) > 0 {
|
|
||||||
logging.Info().Int("count", len(iconCache)).Msg("icon cache loaded")
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
cleanupTicker := time.NewTicker(cleanUpInterval)
|
|
||||||
defer cleanupTicker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-task.RootContextCanceled():
|
|
||||||
return
|
|
||||||
case <-cleanupTicker.C:
|
|
||||||
pruneExpiredIconCache()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
task.OnProgramExit("save_favicon_cache", func() {
|
|
||||||
iconCacheMu.Lock()
|
|
||||||
defer iconCacheMu.Unlock()
|
|
||||||
|
|
||||||
if len(iconCache) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := utils.SaveJSON(common.IconCachePath, &iconCache, 0o644); err != nil {
|
|
||||||
logging.Error().Err(err).Msg("failed to save icon cache")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func pruneExpiredIconCache() {
|
|
||||||
iconCacheMu.Lock()
|
|
||||||
defer iconCacheMu.Unlock()
|
|
||||||
|
|
||||||
nPruned := 0
|
|
||||||
for key, icon := range iconCache {
|
|
||||||
if icon.IsExpired() {
|
|
||||||
delete(iconCache, key)
|
|
||||||
nPruned++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if nPruned > 0 {
|
|
||||||
logging.Info().Int("pruned", nPruned).Msg("pruned expired icon cache")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func routeKey(r route.HTTPRoute) string {
|
|
||||||
return r.ProviderName() + ":" + r.TargetName()
|
|
||||||
}
|
|
||||||
|
|
||||||
func PruneRouteIconCache(route route.HTTPRoute) {
|
|
||||||
iconCacheMu.Lock()
|
|
||||||
defer iconCacheMu.Unlock()
|
|
||||||
delete(iconCache, routeKey(route))
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadIconCache(key string) *fetchResult {
|
|
||||||
iconCacheMu.RLock()
|
|
||||||
defer iconCacheMu.RUnlock()
|
|
||||||
|
|
||||||
icon, ok := iconCache[key]
|
|
||||||
if ok && icon != nil {
|
|
||||||
logging.Debug().
|
|
||||||
Str("key", key).
|
|
||||||
Msg("icon found in cache")
|
|
||||||
icon.LastAccess = time.Now()
|
|
||||||
return &fetchResult{icon: icon.Icon}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func storeIconCache(key string, icon []byte) {
|
|
||||||
iconCacheMu.Lock()
|
|
||||||
defer iconCacheMu.Unlock()
|
|
||||||
iconCache[key] = &cacheEntry{Icon: icon, LastAccess: time.Now()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *cacheEntry) IsExpired() bool {
|
|
||||||
return time.Since(e.LastAccess) > iconCacheTTL
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *cacheEntry) UnmarshalJSON(data []byte) error {
|
|
||||||
attempt := struct {
|
|
||||||
Icon []byte `json:"icon"`
|
|
||||||
LastAccess time.Time `json:"lastAccess"`
|
|
||||||
}{}
|
|
||||||
err := json.Unmarshal(data, &attempt)
|
|
||||||
if err == nil {
|
|
||||||
e.Icon = attempt.Icon
|
|
||||||
e.LastAccess = attempt.LastAccess
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// fallback to bytes
|
|
||||||
err = json.Unmarshal(data, &e.Icon)
|
|
||||||
if err == nil {
|
|
||||||
e.LastAccess = time.Now()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,49 +1,13 @@
|
|||||||
package favicon
|
package favicon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
|
||||||
"github.com/vincent-petithory/dataurl"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/homepage"
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
|
|
||||||
"github.com/yusing/go-proxy/internal/route/routes"
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
route "github.com/yusing/go-proxy/internal/route/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fetchResult struct {
|
|
||||||
icon []byte
|
|
||||||
contentType string
|
|
||||||
statusCode int
|
|
||||||
errMsg string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (res *fetchResult) OK() bool {
|
|
||||||
return res.icon != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (res *fetchResult) ContentType() string {
|
|
||||||
if res.contentType == "" {
|
|
||||||
if bytes.HasPrefix(res.icon, []byte("<svg")) || bytes.HasPrefix(res.icon, []byte("<?xml")) {
|
|
||||||
return "image/svg+xml"
|
|
||||||
}
|
|
||||||
return "image/x-icon"
|
|
||||||
}
|
|
||||||
return res.contentType
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
MaxRedirectDepth = 5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetFavIcon returns the favicon of the route
|
// GetFavIcon returns the favicon of the route
|
||||||
@@ -72,213 +36,42 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
|||||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
gphttp.ClientError(w, err, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fetchResult := getFavIconFromURL(&iconURL)
|
fetchResult := homepage.FetchFavIconFromURL(req.Context(), &iconURL)
|
||||||
if !fetchResult.OK() {
|
if !fetchResult.OK() {
|
||||||
http.Error(w, fetchResult.errMsg, fetchResult.statusCode)
|
http.Error(w, fetchResult.ErrMsg, fetchResult.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", fetchResult.ContentType())
|
w.Header().Set("Content-Type", fetchResult.ContentType())
|
||||||
gphttp.WriteBody(w, fetchResult.icon)
|
gphttp.WriteBody(w, fetchResult.Icon)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// try with route.Homepage.Icon
|
// try with route.Icon
|
||||||
r, ok := routes.GetHTTPRoute(alias)
|
r, ok := routes.HTTP.Get(alias)
|
||||||
if !ok {
|
if !ok {
|
||||||
gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound)
|
gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var result *fetchResult
|
var result *homepage.FetchResult
|
||||||
hp := r.HomepageItem()
|
hp := r.HomepageItem()
|
||||||
if hp.Icon != nil {
|
if hp.Icon != nil {
|
||||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||||
result = findIcon(r, req, hp.Icon.Value)
|
result = homepage.FindIcon(req.Context(), r, hp.Icon.Value)
|
||||||
} else {
|
} else {
|
||||||
result = getFavIconFromURL(hp.Icon)
|
result = homepage.FetchFavIconFromURL(req.Context(), hp.Icon)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// try extract from "link[rel=icon]"
|
// try extract from "link[rel=icon]"
|
||||||
result = findIcon(r, req, "/")
|
result = homepage.FindIcon(req.Context(), r, "/")
|
||||||
}
|
}
|
||||||
if result.statusCode == 0 {
|
if result.StatusCode == 0 {
|
||||||
result.statusCode = http.StatusOK
|
result.StatusCode = http.StatusOK
|
||||||
}
|
}
|
||||||
if !result.OK() {
|
if !result.OK() {
|
||||||
http.Error(w, result.errMsg, result.statusCode)
|
http.Error(w, result.ErrMsg, result.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", result.ContentType())
|
w.Header().Set("Content-Type", result.ContentType())
|
||||||
gphttp.WriteBody(w, result.icon)
|
gphttp.WriteBody(w, result.Icon)
|
||||||
}
|
|
||||||
|
|
||||||
func getFavIconFromURL(iconURL *homepage.IconURL) *fetchResult {
|
|
||||||
switch iconURL.IconSource {
|
|
||||||
case homepage.IconSourceAbsolute:
|
|
||||||
return fetchIconAbsolute(iconURL.URL())
|
|
||||||
case homepage.IconSourceRelative:
|
|
||||||
return &fetchResult{statusCode: http.StatusBadRequest, errMsg: "unexpected relative icon"}
|
|
||||||
case homepage.IconSourceWalkXCode, homepage.IconSourceSelfhSt:
|
|
||||||
return fetchKnownIcon(iconURL)
|
|
||||||
}
|
|
||||||
return &fetchResult{statusCode: http.StatusBadRequest, errMsg: "invalid icon source"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchIconAbsolute(url string) *fetchResult {
|
|
||||||
if result := loadIconCache(url); result != nil {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := gphttp.Get(url)
|
|
||||||
if err != nil || resp.StatusCode != http.StatusOK {
|
|
||||||
if err == nil {
|
|
||||||
err = errors.New(resp.Status)
|
|
||||||
}
|
|
||||||
logging.Error().Err(err).
|
|
||||||
Str("url", url).
|
|
||||||
Msg("failed to get icon")
|
|
||||||
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "connection error"}
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
icon, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
logging.Error().Err(err).
|
|
||||||
Str("url", url).
|
|
||||||
Msg("failed to read icon")
|
|
||||||
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"}
|
|
||||||
}
|
|
||||||
|
|
||||||
storeIconCache(url, icon)
|
|
||||||
return &fetchResult{icon: icon}
|
|
||||||
}
|
|
||||||
|
|
||||||
var nameSanitizer = strings.NewReplacer(
|
|
||||||
"_", "-",
|
|
||||||
" ", "-",
|
|
||||||
"(", "",
|
|
||||||
")", "",
|
|
||||||
)
|
|
||||||
|
|
||||||
func sanitizeName(name string) string {
|
|
||||||
return strings.ToLower(nameSanitizer.Replace(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchKnownIcon(url *homepage.IconURL) *fetchResult {
|
|
||||||
// if icon isn't in the list, no need to fetch
|
|
||||||
if !url.HasIcon() {
|
|
||||||
logging.Debug().
|
|
||||||
Str("value", url.String()).
|
|
||||||
Str("url", url.URL()).
|
|
||||||
Msg("no such icon")
|
|
||||||
return &fetchResult{statusCode: http.StatusNotFound, errMsg: "no such icon"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetchIconAbsolute(url.URL())
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchIcon(filetype, filename string) *fetchResult {
|
|
||||||
result := fetchKnownIcon(homepage.NewSelfhStIconURL(filename, filetype))
|
|
||||||
if result.icon == nil {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return fetchKnownIcon(homepage.NewWalkXCodeIconURL(filename, filetype))
|
|
||||||
}
|
|
||||||
|
|
||||||
func findIcon(r route.HTTPRoute, req *http.Request, uri string) *fetchResult {
|
|
||||||
key := routeKey(r)
|
|
||||||
if result := loadIconCache(key); result != nil {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
result := fetchIcon("png", sanitizeName(r.TargetName()))
|
|
||||||
cont := r.ContainerInfo()
|
|
||||||
if !result.OK() && cont != nil {
|
|
||||||
result = fetchIcon("png", sanitizeName(cont.Image.Name))
|
|
||||||
}
|
|
||||||
if !result.OK() {
|
|
||||||
// fallback to parse html
|
|
||||||
result = findIconSlow(r, req, uri, 0)
|
|
||||||
}
|
|
||||||
if result.OK() {
|
|
||||||
storeIconCache(key, result.icon)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func findIconSlow(r route.HTTPRoute, req *http.Request, uri string, depth int) *fetchResult {
|
|
||||||
ctx, cancel := context.WithTimeoutCause(req.Context(), 3*time.Second, errors.New("favicon request timeout"))
|
|
||||||
defer cancel()
|
|
||||||
newReq := req.WithContext(ctx)
|
|
||||||
newReq.Header.Set("Accept-Encoding", "identity") // disable compression
|
|
||||||
u, err := url.ParseRequestURI(strutils.SanitizeURI(uri))
|
|
||||||
if err != nil {
|
|
||||||
logging.Error().Err(err).
|
|
||||||
Str("route", r.TargetName()).
|
|
||||||
Str("path", uri).
|
|
||||||
Msg("failed to parse uri")
|
|
||||||
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "cannot parse uri"}
|
|
||||||
}
|
|
||||||
newReq.URL.Path = u.Path
|
|
||||||
newReq.URL.RawPath = u.RawPath
|
|
||||||
newReq.URL.RawQuery = u.RawQuery
|
|
||||||
newReq.RequestURI = u.String()
|
|
||||||
|
|
||||||
c := newContent()
|
|
||||||
r.ServeHTTP(c, newReq)
|
|
||||||
if c.status != http.StatusOK {
|
|
||||||
switch c.status {
|
|
||||||
case 0:
|
|
||||||
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "connection error"}
|
|
||||||
default:
|
|
||||||
if loc := c.Header().Get("Location"); loc != "" {
|
|
||||||
if depth > MaxRedirectDepth {
|
|
||||||
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "too many redirects"}
|
|
||||||
}
|
|
||||||
loc = strutils.SanitizeURI(loc)
|
|
||||||
if loc == "/" || loc == newReq.URL.Path {
|
|
||||||
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "circular redirect"}
|
|
||||||
}
|
|
||||||
return findIconSlow(r, req, loc, depth+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &fetchResult{statusCode: c.status, errMsg: "upstream error: " + string(c.data)}
|
|
||||||
}
|
|
||||||
// return icon data
|
|
||||||
if !gphttp.GetContentType(c.header).IsHTML() {
|
|
||||||
return &fetchResult{icon: c.data, contentType: c.header.Get("Content-Type")}
|
|
||||||
}
|
|
||||||
// try extract from "link[rel=icon]" from path "/"
|
|
||||||
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(c.data))
|
|
||||||
if err != nil {
|
|
||||||
logging.Error().Err(err).
|
|
||||||
Str("route", r.TargetName()).
|
|
||||||
Msg("failed to parse html")
|
|
||||||
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"}
|
|
||||||
}
|
|
||||||
ele := doc.Find("head > link[rel=icon]").First()
|
|
||||||
if ele.Length() == 0 {
|
|
||||||
return &fetchResult{statusCode: http.StatusNotFound, errMsg: "icon element not found"}
|
|
||||||
}
|
|
||||||
href := ele.AttrOr("href", "")
|
|
||||||
if href == "" {
|
|
||||||
return &fetchResult{statusCode: http.StatusNotFound, errMsg: "icon href not found"}
|
|
||||||
}
|
|
||||||
// https://en.wikipedia.org/wiki/Data_URI_scheme
|
|
||||||
if strings.HasPrefix(href, "data:image/") {
|
|
||||||
dataURI, err := dataurl.DecodeString(href)
|
|
||||||
if err != nil {
|
|
||||||
logging.Error().Err(err).
|
|
||||||
Str("route", r.TargetName()).
|
|
||||||
Msg("failed to decode favicon")
|
|
||||||
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"}
|
|
||||||
}
|
|
||||||
return &fetchResult{icon: dataURI.Data, contentType: dataURI.ContentType()}
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(href, "http://"), strings.HasPrefix(href, "https://"):
|
|
||||||
return fetchIconAbsolute(href)
|
|
||||||
default:
|
|
||||||
return findIconSlow(r, req, href, 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ import (
|
|||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Health(w http.ResponseWriter, r *http.Request) {
|
func Health(w http.ResponseWriter, r *http.Request) {
|
||||||
if httpheaders.IsWebsocket(r.Header) {
|
if httpheaders.IsWebsocket(r.Header) {
|
||||||
gpwebsocket.Periodic(w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
gpwebsocket.Periodic(w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
||||||
return wsjson.Write(r.Context(), conn, routequery.HealthMap())
|
return wsjson.Write(r.Context(), conn, routes.HealthMap())
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
gphttp.RespondJSON(w, r, routequery.HealthMap())
|
gphttp.RespondJSON(w, r, routes.HealthMap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
||||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
route "github.com/yusing/go-proxy/internal/route/types"
|
route "github.com/yusing/go-proxy/internal/route/types"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
@@ -47,7 +47,7 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
|||||||
gphttp.RespondJSON(w, r, route)
|
gphttp.RespondJSON(w, r, route)
|
||||||
}
|
}
|
||||||
case ListRoutes:
|
case ListRoutes:
|
||||||
gphttp.RespondJSON(w, r, routequery.RoutesByAlias(route.RouteType(r.FormValue("type"))))
|
gphttp.RespondJSON(w, r, routes.ByAlias(route.RouteType(r.FormValue("type"))))
|
||||||
case ListFiles:
|
case ListFiles:
|
||||||
listFiles(w, r)
|
listFiles(w, r)
|
||||||
case ListMiddlewares:
|
case ListMiddlewares:
|
||||||
@@ -57,17 +57,17 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
|||||||
case ListMatchDomains:
|
case ListMatchDomains:
|
||||||
gphttp.RespondJSON(w, r, cfg.Value().MatchDomains)
|
gphttp.RespondJSON(w, r, cfg.Value().MatchDomains)
|
||||||
case ListHomepageConfig:
|
case ListHomepageConfig:
|
||||||
gphttp.RespondJSON(w, r, routequery.HomepageConfig(r.FormValue("category"), r.FormValue("provider")))
|
gphttp.RespondJSON(w, r, routes.HomepageConfig(r.FormValue("category"), r.FormValue("provider")))
|
||||||
case ListRouteProviders:
|
case ListRouteProviders:
|
||||||
gphttp.RespondJSON(w, r, cfg.RouteProviderList())
|
gphttp.RespondJSON(w, r, cfg.RouteProviderList())
|
||||||
case ListHomepageCategories:
|
case ListHomepageCategories:
|
||||||
gphttp.RespondJSON(w, r, routequery.HomepageCategories())
|
gphttp.RespondJSON(w, r, routes.HomepageCategories())
|
||||||
case ListIcons:
|
case ListIcons:
|
||||||
limit, err := strconv.Atoi(r.FormValue("limit"))
|
limit, err := strconv.Atoi(r.FormValue("limit"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
limit = 0
|
limit = 0
|
||||||
}
|
}
|
||||||
icons, err := internal.SearchIcons(r.FormValue("keyword"), limit)
|
icons, err := homepage.SearchIcons(r.FormValue("keyword"), limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gphttp.ClientError(w, err)
|
gphttp.ClientError(w, err)
|
||||||
return
|
return
|
||||||
@@ -87,9 +87,9 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
|||||||
// otherwise, return a single Route with alias which or nil if not found.
|
// otherwise, return a single Route with alias which or nil if not found.
|
||||||
func listRoute(which string) any {
|
func listRoute(which string) any {
|
||||||
if which == "" || which == "all" {
|
if which == "" || which == "all" {
|
||||||
return routequery.RoutesByAlias()
|
return routes.ByAlias()
|
||||||
}
|
}
|
||||||
routes := routequery.RoutesByAlias()
|
routes := routes.ByAlias()
|
||||||
route, ok := routes[which]
|
route, ok := routes[which]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/yusing/go-proxy/agent/pkg/certs"
|
"github.com/yusing/go-proxy/agent/pkg/certs"
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAgent(w http.ResponseWriter, r *http.Request) {
|
func NewAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -56,7 +55,7 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nightly := strutils.ParseBool(q.Get("nightly"))
|
nightly, _ := strconv.ParseBool(q.Get("nightly"))
|
||||||
var image string
|
var image string
|
||||||
if nightly {
|
if nightly {
|
||||||
image = agent.DockerImageNightly
|
image = agent.DockerImageNightly
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -40,7 +41,7 @@ func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
gphttp.WriteBody(w, respData)
|
gphttp.WriteBody(w, respData)
|
||||||
} else {
|
} else {
|
||||||
rp := reverseproxy.NewReverseProxy("agent", agentPkg.AgentURL, agent.Transport())
|
rp := reverseproxy.NewReverseProxy("agent", types.NewURL(agentPkg.AgentURL), agent.Transport())
|
||||||
header := r.Header.Clone()
|
header := r.Header.Clone()
|
||||||
r, err := http.NewRequestWithContext(r.Context(), r.Method, agentPkg.EndpointSystemInfo+"?"+query.Encode(), nil)
|
r, err := http.NewRequestWithContext(r.Context(), r.Method, agentPkg.EndpointSystemInfo+"?"+query.Encode(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
package v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
|
||||||
"github.com/yusing/go-proxy/pkg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetVersion(w http.ResponseWriter, r *http.Request) {
|
|
||||||
gphttp.WriteBody(w, []byte(pkg.GetVersion()))
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
@@ -38,15 +39,41 @@ func IsOIDCEnabled() bool {
|
|||||||
return common.OIDCIssuerURL != ""
|
return common.OIDCIssuerURL != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type nextHandler struct{}
|
||||||
|
|
||||||
|
var nextHandlerContextKey = nextHandler{}
|
||||||
|
|
||||||
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||||
if IsEnabled() {
|
if !IsEnabled() {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return next
|
||||||
if err := defaultAuth.CheckToken(r); err != nil {
|
}
|
||||||
gphttp.ClientError(w, err, http.StatusUnauthorized)
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
} else {
|
if err := defaultAuth.CheckToken(r); err != nil {
|
||||||
next(w, r)
|
if IsFrontend(r) {
|
||||||
}
|
r = r.WithContext(context.WithValue(r.Context(), nextHandlerContextKey, next))
|
||||||
}
|
defaultAuth.LoginHandler(w, r)
|
||||||
|
} else {
|
||||||
|
gphttp.ClientError(w, err, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProceedNext(w http.ResponseWriter, r *http.Request) {
|
||||||
|
next, ok := r.Context().Value(nextHandlerContextKey).(http.HandlerFunc)
|
||||||
|
if ok {
|
||||||
|
next(w, r)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := defaultAuth.CheckToken(r); err != nil {
|
||||||
|
defaultAuth.LoginHandler(w, r)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
return next
|
|
||||||
}
|
}
|
||||||
224
internal/auth/oauth_refresh.go
Normal file
224
internal/auth/oauth_refresh.go
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/jsonstore"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type oauthRefreshToken struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
Expiry time.Time `json:"expiry"`
|
||||||
|
|
||||||
|
result *refreshResult
|
||||||
|
err error
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
SessionID sessionID `json:"session_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type refreshResult struct {
|
||||||
|
newSession Session
|
||||||
|
jwt string
|
||||||
|
jwtExpiry time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionClaims struct {
|
||||||
|
Session
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionID string
|
||||||
|
|
||||||
|
var oauthRefreshTokens jsonstore.MapStore[*oauthRefreshToken]
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultRefreshTokenExpiry = 30 * 24 * time.Hour // 1 month
|
||||||
|
refreshBefore = 30 * time.Second
|
||||||
|
sessionInvalidateDelay = 3 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNoRefreshToken = errors.New("no refresh token")
|
||||||
|
ErrRefreshTokenFailure = errors.New("failed to refresh token")
|
||||||
|
)
|
||||||
|
|
||||||
|
const sessionTokenIssuer = "GoDoxy"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if IsOIDCEnabled() {
|
||||||
|
oauthRefreshTokens = jsonstore.Store[*oauthRefreshToken]("oauth_refresh_tokens")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (token *oauthRefreshToken) expired() bool {
|
||||||
|
return time.Now().After(token.Expiry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSessionID() sessionID {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
_, _ = rand.Read(b)
|
||||||
|
return sessionID(hex.EncodeToString(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSession(username string, groups []string) Session {
|
||||||
|
return Session{
|
||||||
|
SessionID: newSessionID(),
|
||||||
|
Username: username,
|
||||||
|
Groups: groups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOAuthRefreshToken returns the refresh token for the given session.
|
||||||
|
//
|
||||||
|
// The token is removed from the store after retrieval.
|
||||||
|
func getOAuthRefreshToken(claims *Session) (*oauthRefreshToken, bool) {
|
||||||
|
token, ok := oauthRefreshTokens.Load(string(claims.SessionID))
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.expired() {
|
||||||
|
invalidateOAuthRefreshToken(claims.SessionID)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Username != token.Username {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return token, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeOAuthRefreshToken(sessionID sessionID, username, token string) {
|
||||||
|
oauthRefreshTokens.Store(string(sessionID), &oauthRefreshToken{
|
||||||
|
Username: username,
|
||||||
|
RefreshToken: token,
|
||||||
|
Expiry: time.Now().Add(defaultRefreshTokenExpiry),
|
||||||
|
})
|
||||||
|
logging.Debug().Str("username", username).Msg("stored oauth refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidateOAuthRefreshToken(sessionID sessionID) {
|
||||||
|
logging.Debug().Str("session_id", string(sessionID)).Msg("invalidating oauth refresh token")
|
||||||
|
oauthRefreshTokens.Delete(string(sessionID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) setSessionTokenCookie(w http.ResponseWriter, r *http.Request, session Session) {
|
||||||
|
claims := &sessionClaims{
|
||||||
|
Session: session,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
Issuer: sessionTokenIssuer,
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(common.APIJWTTokenTTL)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
||||||
|
signed, err := jwtToken.SignedString(common.APIJWTSecret)
|
||||||
|
if err != nil {
|
||||||
|
logging.Err(err).Msg("failed to sign session token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTokenCookie(w, r, CookieOauthSessionToken, signed, common.APIJWTTokenTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionClaims, valid bool, err error) {
|
||||||
|
claims = &sessionClaims{}
|
||||||
|
sessionToken, err := jwt.ParseWithClaims(sessionJWT, claims, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
|
}
|
||||||
|
return common.APIJWTSecret, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return claims, sessionToken.Valid && claims.Issuer == sessionTokenIssuer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) TryRefreshToken(ctx context.Context, sessionJWT string) (*refreshResult, error) {
|
||||||
|
// verify the session cookie
|
||||||
|
claims, valid, err := auth.parseSessionJWT(sessionJWT)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrInvalidSessionToken, err)
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return nil, ErrInvalidSessionToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if refresh is possible
|
||||||
|
refreshToken, ok := getOAuthRefreshToken(&claims.Session)
|
||||||
|
if !ok {
|
||||||
|
return nil, errNoRefreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if !auth.checkAllowed(claims.Username, claims.Groups) {
|
||||||
|
return nil, ErrUserNotAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth.doRefreshToken(ctx, refreshToken, &claims.Session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) doRefreshToken(ctx context.Context, refreshToken *oauthRefreshToken, claims *Session) (*refreshResult, error) {
|
||||||
|
refreshToken.mu.Lock()
|
||||||
|
defer refreshToken.mu.Unlock()
|
||||||
|
|
||||||
|
// already refreshed
|
||||||
|
// this must be called after refresh but before invalidate
|
||||||
|
if refreshToken.result != nil || refreshToken.err != nil {
|
||||||
|
return refreshToken.result, refreshToken.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// this step refreshes the token
|
||||||
|
// see https://cs.opensource.google/go/x/oauth2/+/refs/tags/v0.29.0:oauth2.go;l=313
|
||||||
|
newToken, err := auth.oauthConfig.TokenSource(ctx, &oauth2.Token{
|
||||||
|
RefreshToken: refreshToken.RefreshToken,
|
||||||
|
}).Token()
|
||||||
|
if err != nil {
|
||||||
|
refreshToken.err = fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrRefreshTokenFailure, err)
|
||||||
|
return nil, refreshToken.err
|
||||||
|
}
|
||||||
|
|
||||||
|
idTokenJWT, idToken, err := auth.getIdToken(ctx, newToken)
|
||||||
|
if err != nil {
|
||||||
|
refreshToken.err = fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrRefreshTokenFailure, err)
|
||||||
|
return nil, refreshToken.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case there're multiple requests for the same session to refresh
|
||||||
|
// invalidate the token after a short delay
|
||||||
|
go func() {
|
||||||
|
<-time.After(sessionInvalidateDelay)
|
||||||
|
invalidateOAuthRefreshToken(claims.SessionID)
|
||||||
|
}()
|
||||||
|
|
||||||
|
sessionID := newSessionID()
|
||||||
|
|
||||||
|
logging.Debug().Str("username", claims.Username).Time("expiry", newToken.Expiry).Msg("refreshed token")
|
||||||
|
storeOAuthRefreshToken(sessionID, claims.Username, newToken.RefreshToken)
|
||||||
|
|
||||||
|
refreshToken.result = &refreshResult{
|
||||||
|
newSession: Session{
|
||||||
|
SessionID: sessionID,
|
||||||
|
Username: claims.Username,
|
||||||
|
Groups: claims.Groups,
|
||||||
|
},
|
||||||
|
jwt: idTokenJWT,
|
||||||
|
jwtExpiry: idToken.Expiry,
|
||||||
|
}
|
||||||
|
return refreshToken.result, nil
|
||||||
|
}
|
||||||
325
internal/auth/oidc.go
Normal file
325
internal/auth/oidc.go
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
OIDCProvider struct {
|
||||||
|
oauthConfig *oauth2.Config
|
||||||
|
oidcProvider *oidc.Provider
|
||||||
|
oidcVerifier *oidc.IDTokenVerifier
|
||||||
|
endSessionURL *url.URL
|
||||||
|
allowedUsers []string
|
||||||
|
allowedGroups []string
|
||||||
|
}
|
||||||
|
|
||||||
|
IDTokenClaims struct {
|
||||||
|
Username string `json:"preferred_username"`
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CookieOauthState = "godoxy_oidc_state"
|
||||||
|
CookieOauthToken = "godoxy_oauth_token"
|
||||||
|
CookieOauthSessionToken = "godoxy_session_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OIDCAuthInitPath = "/"
|
||||||
|
OIDCPostAuthPath = "/auth/callback"
|
||||||
|
OIDCLogoutPath = "/auth/logout"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errMissingIDToken = errors.New("missing id_token field from oauth token")
|
||||||
|
|
||||||
|
ErrMissingOAuthToken = gperr.New("missing oauth token")
|
||||||
|
ErrInvalidOAuthToken = gperr.New("invalid oauth token")
|
||||||
|
)
|
||||||
|
|
||||||
|
// generateState generates a random string for OIDC state.
|
||||||
|
const oidcStateLength = 32
|
||||||
|
|
||||||
|
func generateState() string {
|
||||||
|
b := make([]byte, oidcStateLength)
|
||||||
|
_, _ = rand.Read(b)
|
||||||
|
return base64.URLEncoding.EncodeToString(b)[:oidcStateLength]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, allowedGroups []string) (*OIDCProvider, error) {
|
||||||
|
if len(allowedUsers)+len(allowedGroups) == 0 {
|
||||||
|
return nil, errors.New("oidc.allowed_users or oidc.allowed_groups are both empty")
|
||||||
|
}
|
||||||
|
provider, err := oidc.NewProvider(context.Background(), issuerURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endSessionURL, err := url.Parse(provider.EndSessionEndpoint())
|
||||||
|
if err != nil && provider.EndSessionEndpoint() != "" {
|
||||||
|
// non critical, just warn
|
||||||
|
logging.Warn().
|
||||||
|
Str("issuer", issuerURL).
|
||||||
|
Err(err).
|
||||||
|
Msg("failed to parse end session URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OIDCProvider{
|
||||||
|
oauthConfig: &oauth2.Config{
|
||||||
|
ClientID: clientID,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
RedirectURL: "",
|
||||||
|
Endpoint: provider.Endpoint(),
|
||||||
|
Scopes: common.OIDCScopes,
|
||||||
|
},
|
||||||
|
oidcProvider: provider,
|
||||||
|
oidcVerifier: provider.Verifier(&oidc.Config{
|
||||||
|
ClientID: clientID,
|
||||||
|
}),
|
||||||
|
endSessionURL: endSessionURL,
|
||||||
|
allowedUsers: allowedUsers,
|
||||||
|
allowedGroups: allowedGroups,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOIDCProviderFromEnv creates a new OIDCProvider from environment variables.
|
||||||
|
func NewOIDCProviderFromEnv() (*OIDCProvider, error) {
|
||||||
|
return NewOIDCProvider(
|
||||||
|
common.OIDCIssuerURL,
|
||||||
|
common.OIDCClientID,
|
||||||
|
common.OIDCClientSecret,
|
||||||
|
common.OIDCAllowedUsers,
|
||||||
|
common.OIDCAllowedGroups,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) SetAllowedUsers(users []string) {
|
||||||
|
auth.allowedUsers = users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) SetAllowedGroups(groups []string) {
|
||||||
|
auth.allowedGroups = groups
|
||||||
|
}
|
||||||
|
|
||||||
|
// optRedirectPostAuth returns an oauth2 option that sets the "redirect_uri"
|
||||||
|
// parameter of the authorization URL to the post auth path of the current
|
||||||
|
// request host.
|
||||||
|
func optRedirectPostAuth(r *http.Request) oauth2.AuthCodeOption {
|
||||||
|
return oauth2.SetAuthURLParam("redirect_uri", "https://"+requestHost(r)+OIDCPostAuthPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) getIdToken(ctx context.Context, oauthToken *oauth2.Token) (string, *oidc.IDToken, error) {
|
||||||
|
idTokenJWT, ok := oauthToken.Extra("id_token").(string)
|
||||||
|
if !ok {
|
||||||
|
return "", nil, errMissingIDToken
|
||||||
|
}
|
||||||
|
idToken, err := auth.oidcVerifier.Verify(ctx, idTokenJWT)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("failed to verify ID token: %w", err)
|
||||||
|
}
|
||||||
|
return idTokenJWT, idToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case OIDCAuthInitPath:
|
||||||
|
auth.LoginHandler(w, r)
|
||||||
|
case OIDCPostAuthPath:
|
||||||
|
auth.PostAuthCallbackHandler(w, r)
|
||||||
|
case OIDCLogoutPath:
|
||||||
|
auth.LogoutHandler(w, r)
|
||||||
|
default:
|
||||||
|
http.Redirect(w, r, OIDCAuthInitPath, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// check for session token
|
||||||
|
sessionToken, err := r.Cookie(CookieOauthSessionToken)
|
||||||
|
if err == nil { // session token exists
|
||||||
|
result, err := auth.TryRefreshToken(r.Context(), sessionToken.Value)
|
||||||
|
// redirect back to where they requested
|
||||||
|
// when token refresh is ok
|
||||||
|
if err == nil {
|
||||||
|
auth.setIDTokenCookie(w, r, result.jwt, time.Until(result.jwtExpiry))
|
||||||
|
auth.setSessionTokenCookie(w, r, result.newSession)
|
||||||
|
ProceedNext(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// clear cookies then redirect to home
|
||||||
|
logging.Err(err).Msg("failed to refresh token")
|
||||||
|
auth.clearCookie(w, r)
|
||||||
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := generateState()
|
||||||
|
setTokenCookie(w, r, CookieOauthState, state, 300*time.Second)
|
||||||
|
// redirect user to Idp
|
||||||
|
http.Redirect(w, r, auth.oauthConfig.AuthCodeURL(state, optRedirectPostAuth(r)), http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseClaims(idToken *oidc.IDToken) (*IDTokenClaims, error) {
|
||||||
|
var claim IDTokenClaims
|
||||||
|
if err := idToken.Claims(&claim); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse claims: %w", err)
|
||||||
|
}
|
||||||
|
if claim.Username == "" {
|
||||||
|
return nil, fmt.Errorf("missing username in ID token")
|
||||||
|
}
|
||||||
|
return &claim, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) checkAllowed(user string, groups []string) bool {
|
||||||
|
userAllowed := slices.Contains(auth.allowedUsers, user)
|
||||||
|
if !userAllowed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(auth.allowedGroups) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return len(utils.Intersect(groups, auth.allowedGroups)) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) CheckToken(r *http.Request) error {
|
||||||
|
tokenCookie, err := r.Cookie(CookieOauthToken)
|
||||||
|
if err != nil {
|
||||||
|
return ErrMissingOAuthToken
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, err := auth.oidcVerifier.Verify(r.Context(), tokenCookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrInvalidOAuthToken, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := parseClaims(idToken)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrInvalidOAuthToken, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !auth.checkAllowed(claims.Username, claims.Groups) {
|
||||||
|
return ErrUserNotAllowed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// For testing purposes, skip provider verification
|
||||||
|
if common.IsTest {
|
||||||
|
auth.handleTestCallback(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify state
|
||||||
|
state, err := r.Cookie(CookieOauthState)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.BadRequest(w, "missing state cookie")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.URL.Query().Get("state") != state.Value {
|
||||||
|
gphttp.BadRequest(w, "invalid oauth state")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code := r.URL.Query().Get("code")
|
||||||
|
oauth2Token, err := auth.oauthConfig.Exchange(r.Context(), code, optRedirectPostAuth(r))
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ServerError(w, r, fmt.Errorf("failed to exchange token: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idTokenJWT, idToken, err := auth.getIdToken(r.Context(), oauth2Token)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if oauth2Token.RefreshToken != "" {
|
||||||
|
claims, err := parseClaims(idToken)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session := newSession(claims.Username, claims.Groups)
|
||||||
|
storeOAuthRefreshToken(session.SessionID, claims.Username, oauth2Token.RefreshToken)
|
||||||
|
auth.setSessionTokenCookie(w, r, session)
|
||||||
|
}
|
||||||
|
auth.setIDTokenCookie(w, r, idTokenJWT, time.Until(idToken.Expiry))
|
||||||
|
|
||||||
|
// Redirect to home page
|
||||||
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
oauthToken, _ := r.Cookie(CookieOauthToken)
|
||||||
|
sessionToken, _ := r.Cookie(CookieOauthSessionToken)
|
||||||
|
auth.clearCookie(w, r)
|
||||||
|
|
||||||
|
if sessionToken != nil {
|
||||||
|
claims, _, err := auth.parseSessionJWT(sessionToken.Value)
|
||||||
|
if err == nil {
|
||||||
|
invalidateOAuthRefreshToken(claims.SessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "/"
|
||||||
|
if auth.endSessionURL != nil && oauthToken != nil {
|
||||||
|
query := auth.endSessionURL.Query()
|
||||||
|
query.Set("id_token_hint", oauthToken.Value)
|
||||||
|
query.Set("post_logout_redirect_uri", "https://"+requestHost(r))
|
||||||
|
|
||||||
|
clone := *auth.endSessionURL
|
||||||
|
clone.RawQuery = query.Encode()
|
||||||
|
url = clone.String()
|
||||||
|
} else if auth.endSessionURL != nil {
|
||||||
|
url = auth.endSessionURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, url, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) setIDTokenCookie(w http.ResponseWriter, r *http.Request, jwt string, ttl time.Duration) {
|
||||||
|
setTokenCookie(w, r, CookieOauthToken, jwt, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) clearCookie(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clearTokenCookie(w, r, CookieOauthToken)
|
||||||
|
clearTokenCookie(w, r, CookieOauthSessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTestCallback handles OIDC callback in test environment.
|
||||||
|
func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
|
state, err := r.Cookie(CookieOauthState)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.BadRequest(w, "missing state cookie")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Query().Get("state") != state.Value {
|
||||||
|
gphttp.BadRequest(w, "invalid oauth state")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test JWT token
|
||||||
|
setTokenCookie(w, r, CookieOauthToken, "test", time.Hour)
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -35,7 +36,8 @@ func setupMockOIDC(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||||
},
|
},
|
||||||
oidcProvider: provider,
|
endSessionURL: Must(url.Parse("http://mock-provider/logout")),
|
||||||
|
oidcProvider: provider,
|
||||||
oidcVerifier: provider.Verifier(&oidc.Config{
|
oidcVerifier: provider.Verifier(&oidc.Config{
|
||||||
ClientID: "test-client",
|
ClientID: "test-client",
|
||||||
}),
|
}),
|
||||||
@@ -148,17 +150,17 @@ func TestOIDCLoginHandler(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Success - Redirects to provider",
|
name: "Success - Redirects to provider",
|
||||||
wantStatus: http.StatusTemporaryRedirect,
|
wantStatus: http.StatusFound,
|
||||||
wantRedirect: true,
|
wantRedirect: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodGet, "/auth/redirect", nil)
|
req := httptest.NewRequest(http.MethodGet, OIDCAuthInitPath, nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
defaultAuth.RedirectLoginPage(w, req)
|
defaultAuth.(*OIDCProvider).HandleAuth(w, req)
|
||||||
|
|
||||||
if got := w.Code; got != tt.wantStatus {
|
if got := w.Code; got != tt.wantStatus {
|
||||||
t.Errorf("OIDCLoginHandler() status = %v, want %v", got, tt.wantStatus)
|
t.Errorf("OIDCLoginHandler() status = %v, want %v", got, tt.wantStatus)
|
||||||
@@ -194,7 +196,7 @@ func TestOIDCCallbackHandler(t *testing.T) {
|
|||||||
state: "valid-state",
|
state: "valid-state",
|
||||||
code: "valid-code",
|
code: "valid-code",
|
||||||
setupMocks: true,
|
setupMocks: true,
|
||||||
wantStatus: http.StatusTemporaryRedirect,
|
wantStatus: http.StatusFound,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Failure - Missing state",
|
name: "Failure - Missing state",
|
||||||
@@ -219,7 +221,7 @@ func TestOIDCCallbackHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
defaultAuth.LoginCallbackHandler(w, req)
|
defaultAuth.(*OIDCProvider).PostAuthCallbackHandler(w, req)
|
||||||
|
|
||||||
if got := w.Code; got != tt.wantStatus {
|
if got := w.Code; got != tt.wantStatus {
|
||||||
t.Errorf("OIDCCallbackHandler() status = %v, want %v", got, tt.wantStatus)
|
t.Errorf("OIDCCallbackHandler() status = %v, want %v", got, tt.wantStatus)
|
||||||
@@ -227,7 +229,7 @@ func TestOIDCCallbackHandler(t *testing.T) {
|
|||||||
|
|
||||||
if tt.wantStatus == http.StatusTemporaryRedirect {
|
if tt.wantStatus == http.StatusTemporaryRedirect {
|
||||||
setCookie := Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
setCookie := Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
||||||
ExpectEqual(t, setCookie.Name, defaultAuth.TokenCookieName())
|
ExpectEqual(t, setCookie.Name, CookieOauthToken)
|
||||||
ExpectTrue(t, setCookie.Value != "")
|
ExpectTrue(t, setCookie.Value != "")
|
||||||
ExpectEqual(t, setCookie.Path, "/")
|
ExpectEqual(t, setCookie.Path, "/")
|
||||||
ExpectEqual(t, setCookie.SameSite, http.SameSiteLaxMode)
|
ExpectEqual(t, setCookie.SameSite, http.SameSiteLaxMode)
|
||||||
@@ -270,7 +272,6 @@ func TestInitOIDC(t *testing.T) {
|
|||||||
issuerURL: server.URL,
|
issuerURL: server.URL,
|
||||||
clientID: "client_id",
|
clientID: "client_id",
|
||||||
clientSecret: "client_secret",
|
clientSecret: "client_secret",
|
||||||
redirectURL: "https://example.com/callback",
|
|
||||||
allowedUsers: []string{"user1", "user2"},
|
allowedUsers: []string{"user1", "user2"},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
@@ -279,7 +280,6 @@ func TestInitOIDC(t *testing.T) {
|
|||||||
issuerURL: server.URL,
|
issuerURL: server.URL,
|
||||||
clientID: "client_id",
|
clientID: "client_id",
|
||||||
clientSecret: "client_secret",
|
clientSecret: "client_secret",
|
||||||
redirectURL: "https://example.com/callback",
|
|
||||||
allowedGroups: []string{"group1", "group2"},
|
allowedGroups: []string{"group1", "group2"},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
@@ -288,7 +288,6 @@ func TestInitOIDC(t *testing.T) {
|
|||||||
issuerURL: server.URL,
|
issuerURL: server.URL,
|
||||||
clientID: "client_id",
|
clientID: "client_id",
|
||||||
clientSecret: "client_secret",
|
clientSecret: "client_secret",
|
||||||
redirectURL: "https://example.com/callback",
|
|
||||||
logoutURL: "https://example.com/logout",
|
logoutURL: "https://example.com/logout",
|
||||||
allowedUsers: []string{"user1", "user2"},
|
allowedUsers: []string{"user1", "user2"},
|
||||||
allowedGroups: []string{"group1", "group2"},
|
allowedGroups: []string{"group1", "group2"},
|
||||||
@@ -299,14 +298,13 @@ func TestInitOIDC(t *testing.T) {
|
|||||||
issuerURL: "https://example.com",
|
issuerURL: "https://example.com",
|
||||||
clientID: "client_id",
|
clientID: "client_id",
|
||||||
clientSecret: "client_secret",
|
clientSecret: "client_secret",
|
||||||
redirectURL: "https://example.com/callback",
|
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
_, err := NewOIDCProvider(tt.issuerURL, tt.clientID, tt.clientSecret, tt.redirectURL, tt.allowedUsers, tt.allowedGroups)
|
_, err := NewOIDCProvider(tt.issuerURL, tt.clientID, tt.clientSecret, tt.allowedUsers, tt.allowedGroups)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("InitOIDC() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("InitOIDC() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
}
|
}
|
||||||
@@ -400,7 +398,7 @@ func TestCheckToken(t *testing.T) {
|
|||||||
"preferred_username": "user1",
|
"preferred_username": "user1",
|
||||||
"groups": []string{"group1"},
|
"groups": []string{"group1"},
|
||||||
},
|
},
|
||||||
wantErr: ErrInvalidToken,
|
wantErr: ErrInvalidOAuthToken,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Error - Server returns incorrect audience",
|
name: "Error - Server returns incorrect audience",
|
||||||
@@ -411,7 +409,7 @@ func TestCheckToken(t *testing.T) {
|
|||||||
"preferred_username": "user1",
|
"preferred_username": "user1",
|
||||||
"groups": []string{"group1"},
|
"groups": []string{"group1"},
|
||||||
},
|
},
|
||||||
wantErr: ErrInvalidToken,
|
wantErr: ErrInvalidOAuthToken,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Error - Server returns expired token",
|
name: "Error - Server returns expired token",
|
||||||
@@ -422,7 +420,7 @@ func TestCheckToken(t *testing.T) {
|
|||||||
"preferred_username": "user1",
|
"preferred_username": "user1",
|
||||||
"groups": []string{"group1"},
|
"groups": []string{"group1"},
|
||||||
},
|
},
|
||||||
wantErr: ErrInvalidToken,
|
wantErr: ErrInvalidOAuthToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
@@ -438,7 +436,7 @@ func TestCheckToken(t *testing.T) {
|
|||||||
// Craft a test HTTP request that includes the token as a cookie.
|
// Craft a test HTTP request that includes the token as a cookie.
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
req.AddCookie(&http.Cookie{
|
req.AddCookie(&http.Cookie{
|
||||||
Name: auth.TokenCookieName(),
|
Name: CookieOauthToken,
|
||||||
Value: signedToken,
|
Value: signedToken,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -452,3 +450,35 @@ func TestCheckToken(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLogoutHandler(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
setupMockOIDC(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, OIDCLogoutPath, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
req.AddCookie(&http.Cookie{
|
||||||
|
Name: CookieOauthToken,
|
||||||
|
Value: "test-token",
|
||||||
|
})
|
||||||
|
req.AddCookie(&http.Cookie{
|
||||||
|
Name: CookieOauthSessionToken,
|
||||||
|
Value: "test-session-token",
|
||||||
|
})
|
||||||
|
|
||||||
|
defaultAuth.(*OIDCProvider).LogoutHandler(w, req)
|
||||||
|
|
||||||
|
if got := w.Code; got != http.StatusFound {
|
||||||
|
t.Errorf("LogoutHandler() status = %v, want %v", got, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := w.Header().Get("Location"); got == "" {
|
||||||
|
t.Error("LogoutHandler() missing redirect location")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(w.Header().Values("Set-Cookie")) != 2 {
|
||||||
|
t.Error("LogoutHandler() did not clear all cookies")
|
||||||
|
}
|
||||||
|
}
|
||||||
10
internal/auth/provider.go
Normal file
10
internal/auth/provider.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
CheckToken(r *http.Request) error
|
||||||
|
LoginHandler(w http.ResponseWriter, r *http.Request)
|
||||||
|
PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request)
|
||||||
|
LogoutHandler(w http.ResponseWriter, r *http.Request)
|
||||||
|
}
|
||||||
@@ -76,7 +76,7 @@ func (auth *UserPassAuth) NewToken() (token string, err error) {
|
|||||||
func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
||||||
jwtCookie, err := r.Cookie(auth.TokenCookieName())
|
jwtCookie, err := r.Cookie(auth.TokenCookieName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrMissingToken
|
return ErrMissingSessionToken
|
||||||
}
|
}
|
||||||
var claims UserPassClaims
|
var claims UserPassClaims
|
||||||
token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (interface{}, error) {
|
token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (interface{}, error) {
|
||||||
@@ -90,7 +90,7 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
|||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case !token.Valid:
|
case !token.Valid:
|
||||||
return ErrInvalidToken
|
return ErrInvalidSessionToken
|
||||||
case claims.Username != auth.username:
|
case claims.Username != auth.username:
|
||||||
return ErrUserNotAllowed.Subject(claims.Username)
|
return ErrUserNotAllowed.Subject(claims.Username)
|
||||||
case claims.ExpiresAt.Before(time.Now()):
|
case claims.ExpiresAt.Before(time.Now()):
|
||||||
@@ -100,11 +100,7 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *UserPassAuth) RedirectLoginPage(w http.ResponseWriter, r *http.Request) {
|
func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *UserPassAuth) LoginCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var creds struct {
|
var creds struct {
|
||||||
User string `json:"username"`
|
User string `json:"username"`
|
||||||
Pass string `json:"password"`
|
Pass string `json:"password"`
|
||||||
@@ -127,8 +123,13 @@ func (auth *UserPassAuth) LoginCallbackHandler(w http.ResponseWriter, r *http.Re
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *UserPassAuth) LogoutCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
func (auth *UserPassAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
DefaultLogoutCallbackHandler(auth, w, r)
|
http.Redirect(w, r, "/login", http.StatusFound) // redirects to WebUI login page
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clearTokenCookie(w, r, auth.TokenCookieName())
|
||||||
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *UserPassAuth) validatePassword(user, pass string) error {
|
func (auth *UserPassAuth) validatePassword(user, pass string) error {
|
||||||
@@ -98,7 +98,7 @@ func TestUserPassLoginCallbackHandler(t *testing.T) {
|
|||||||
Host: "app.example.com",
|
Host: "app.example.com",
|
||||||
Body: io.NopCloser(bytes.NewReader(Must(json.Marshal(tt.creds)))),
|
Body: io.NopCloser(bytes.NewReader(Must(json.Marshal(tt.creds)))),
|
||||||
}
|
}
|
||||||
auth.LoginCallbackHandler(w, req)
|
auth.LoginHandler(w, req)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
ExpectEqual(t, w.Code, http.StatusUnauthorized)
|
ExpectEqual(t, w.Code, http.StatusUnauthorized)
|
||||||
} else {
|
} else {
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -11,35 +10,33 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrMissingToken = gperr.New("missing token")
|
ErrMissingSessionToken = gperr.New("missing session token")
|
||||||
ErrInvalidToken = gperr.New("invalid token")
|
ErrInvalidSessionToken = gperr.New("invalid session token")
|
||||||
ErrUserNotAllowed = gperr.New("user not allowed")
|
ErrUserNotAllowed = gperr.New("user not allowed")
|
||||||
)
|
)
|
||||||
|
|
||||||
// cookieFQDN returns the fully qualified domain name of the request host
|
func IsFrontend(r *http.Request) bool {
|
||||||
|
return r.Host == common.APIHTTPAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestHost(r *http.Request) string {
|
||||||
|
// check if it's from backend
|
||||||
|
if IsFrontend(r) {
|
||||||
|
return r.Header.Get("X-Forwarded-Host")
|
||||||
|
}
|
||||||
|
return r.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
// cookieDomain returns the fully qualified domain name of the request host
|
||||||
// with subdomain stripped.
|
// with subdomain stripped.
|
||||||
//
|
//
|
||||||
// If the request host does not have a subdomain,
|
// If the request host does not have a subdomain,
|
||||||
// an empty string is returned
|
// an empty string is returned
|
||||||
//
|
//
|
||||||
// "abc.example.com" -> "example.com"
|
// "abc.example.com" -> ".example.com" (cross subdomain)
|
||||||
// "example.com" -> ""
|
// "example.com" -> "" (same domain only)
|
||||||
func cookieFQDN(r *http.Request) string {
|
func cookieDomain(r *http.Request) string {
|
||||||
var host string
|
parts := strutils.SplitRune(requestHost(r), '.')
|
||||||
// check if it's from backend
|
|
||||||
switch r.Host {
|
|
||||||
case common.APIHTTPAddr:
|
|
||||||
// use XFH
|
|
||||||
host = r.Header.Get("X-Forwarded-Host")
|
|
||||||
default:
|
|
||||||
var err error
|
|
||||||
host, _, err = net.SplitHostPort(r.Host)
|
|
||||||
if err != nil {
|
|
||||||
host = r.Host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strutils.SplitRune(host, '.')
|
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -52,7 +49,7 @@ func setTokenCookie(w http.ResponseWriter, r *http.Request, name, value string,
|
|||||||
Name: name,
|
Name: name,
|
||||||
Value: value,
|
Value: value,
|
||||||
MaxAge: int(ttl.Seconds()),
|
MaxAge: int(ttl.Seconds()),
|
||||||
Domain: cookieFQDN(r),
|
Domain: cookieDomain(r),
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: common.APIJWTSecure,
|
Secure: common.APIJWTSecure,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
@@ -65,16 +62,10 @@ func clearTokenCookie(w http.ResponseWriter, r *http.Request, name string) {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Value: "",
|
Value: "",
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Domain: cookieFQDN(r),
|
Domain: cookieDomain(r),
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: common.APIJWTSecure,
|
Secure: common.APIJWTSecure,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultLogoutCallbackHandler clears the token cookie and redirects to the login page..
|
|
||||||
func DefaultLogoutCallbackHandler(auth Provider, w http.ResponseWriter, r *http.Request) {
|
|
||||||
clearTokenCookie(w, r, auth.TokenCookieName())
|
|
||||||
auth.RedirectLoginPage(w, r)
|
|
||||||
}
|
|
||||||
@@ -16,18 +16,15 @@ import (
|
|||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type Config struct {
|
||||||
AutocertConfig struct {
|
Email string `json:"email,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Domains []string `json:"domains,omitempty"`
|
||||||
Domains []string `json:"domains,omitempty"`
|
CertPath string `json:"cert_path,omitempty"`
|
||||||
CertPath string `json:"cert_path,omitempty"`
|
KeyPath string `json:"key_path,omitempty"`
|
||||||
KeyPath string `json:"key_path,omitempty"`
|
ACMEKeyPath string `json:"acme_key_path,omitempty"`
|
||||||
ACMEKeyPath string `json:"acme_key_path,omitempty"`
|
Provider string `json:"provider,omitempty"`
|
||||||
Provider string `json:"provider,omitempty"`
|
Options map[string]any `json:"options,omitempty"`
|
||||||
Options ProviderOpt `json:"options,omitempty"`
|
}
|
||||||
}
|
|
||||||
ProviderOpt map[string]any
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrMissingDomain = gperr.New("missing field 'domains'")
|
ErrMissingDomain = gperr.New("missing field 'domains'")
|
||||||
@@ -37,10 +34,15 @@ var (
|
|||||||
ErrUnknownProvider = gperr.New("unknown provider")
|
ErrUnknownProvider = gperr.New("unknown provider")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProviderLocal = "local"
|
||||||
|
ProviderPseudo = "pseudo"
|
||||||
|
)
|
||||||
|
|
||||||
var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
|
var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
|
||||||
|
|
||||||
// Validate implements the utils.CustomValidator interface.
|
// Validate implements the utils.CustomValidator interface.
|
||||||
func (cfg *AutocertConfig) Validate() gperr.Error {
|
func (cfg *Config) Validate() gperr.Error {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -64,11 +66,11 @@ func (cfg *AutocertConfig) Validate() gperr.Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// check if provider is implemented
|
// check if provider is implemented
|
||||||
providerConstructor, ok := providersGenMap[cfg.Provider]
|
providerConstructor, ok := Providers[cfg.Provider]
|
||||||
if !ok {
|
if !ok {
|
||||||
b.Add(ErrUnknownProvider.
|
b.Add(ErrUnknownProvider.
|
||||||
Subject(cfg.Provider).
|
Subject(cfg.Provider).
|
||||||
Withf(strutils.DoYouMean(utils.NearestField(cfg.Provider, providersGenMap))))
|
Withf(strutils.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
|
||||||
} else {
|
} else {
|
||||||
_, err := providerConstructor(cfg.Options)
|
_, err := providerConstructor(cfg.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -79,13 +81,9 @@ func (cfg *AutocertConfig) Validate() gperr.Error {
|
|||||||
return b.Error()
|
return b.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AutocertConfig) GetProvider() (*Provider, gperr.Error) {
|
func (cfg *Config) GetLegoConfig() (*User, *lego.Config, gperr.Error) {
|
||||||
if cfg == nil {
|
|
||||||
cfg = new(AutocertConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfg.Validate(); err != nil {
|
if err := cfg.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.CertPath == "" {
|
if cfg.CertPath == "" {
|
||||||
@@ -102,35 +100,31 @@ func (cfg *AutocertConfig) GetProvider() (*Provider, gperr.Error) {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if cfg.Provider != ProviderLocal && cfg.Provider != ProviderPseudo {
|
if cfg.Provider != ProviderLocal && cfg.Provider != ProviderPseudo {
|
||||||
if privKey, err = cfg.loadACMEKey(); err != nil {
|
if privKey, err = cfg.LoadACMEKey(); err != nil {
|
||||||
logging.Info().Err(err).Msg("load ACME private key failed")
|
logging.Info().Err(err).Msg("load ACME private key failed")
|
||||||
logging.Info().Msg("generate new ACME private key")
|
logging.Info().Msg("generate new ACME private key")
|
||||||
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gperr.New("generate ACME private key").With(err)
|
return nil, nil, gperr.New("generate ACME private key").With(err)
|
||||||
}
|
}
|
||||||
if err = cfg.saveACMEKey(privKey); err != nil {
|
if err = cfg.SaveACMEKey(privKey); err != nil {
|
||||||
return nil, gperr.New("save ACME private key").With(err)
|
return nil, nil, gperr.New("save ACME private key").With(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &User{
|
user := &User{
|
||||||
Email: cfg.Email,
|
Email: cfg.Email,
|
||||||
key: privKey,
|
Key: privKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
legoCfg := lego.NewConfig(user)
|
legoCfg := lego.NewConfig(user)
|
||||||
legoCfg.Certificate.KeyType = certcrypto.RSA2048
|
legoCfg.Certificate.KeyType = certcrypto.RSA2048
|
||||||
|
|
||||||
return &Provider{
|
return user, legoCfg, nil
|
||||||
cfg: cfg,
|
|
||||||
user: user,
|
|
||||||
legoCfg: legoCfg,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AutocertConfig) loadACMEKey() (*ecdsa.PrivateKey, error) {
|
func (cfg *Config) LoadACMEKey() (*ecdsa.PrivateKey, error) {
|
||||||
data, err := os.ReadFile(cfg.ACMEKeyPath)
|
data, err := os.ReadFile(cfg.ACMEKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -138,7 +132,7 @@ func (cfg *AutocertConfig) loadACMEKey() (*ecdsa.PrivateKey, error) {
|
|||||||
return x509.ParseECPrivateKey(data)
|
return x509.ParseECPrivateKey(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AutocertConfig) saveACMEKey(key *ecdsa.PrivateKey) error {
|
func (cfg *Config) SaveACMEKey(key *ecdsa.PrivateKey) error {
|
||||||
data, err := x509.MarshalECPrivateKey(key)
|
data, err := x509.MarshalECPrivateKey(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package autocert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/porkbun"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
certBasePath = "certs/"
|
|
||||||
CertFileDefault = certBasePath + "cert.crt"
|
|
||||||
KeyFileDefault = certBasePath + "priv.key"
|
|
||||||
ACMEKeyFileDefault = certBasePath + "acme.key"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProviderLocal = "local"
|
|
||||||
ProviderCloudflare = "cloudflare"
|
|
||||||
ProviderClouddns = "clouddns"
|
|
||||||
ProviderDuckdns = "duckdns"
|
|
||||||
ProviderOVH = "ovh"
|
|
||||||
ProviderPseudo = "pseudo" // for testing
|
|
||||||
ProviderPorkbun = "porkbun"
|
|
||||||
)
|
|
||||||
|
|
||||||
var providersGenMap = map[string]ProviderGenerator{
|
|
||||||
ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
|
||||||
ProviderCloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
|
||||||
ProviderClouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
|
||||||
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
|
||||||
ProviderOVH: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
|
|
||||||
ProviderPseudo: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
|
||||||
ProviderPorkbun: providerGenerator(porkbun.NewDefaultConfig, porkbun.NewDNSProviderConfig),
|
|
||||||
}
|
|
||||||
8
internal/autocert/paths.go
Normal file
8
internal/autocert/paths.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package autocert
|
||||||
|
|
||||||
|
const (
|
||||||
|
certBasePath = "certs/"
|
||||||
|
CertFileDefault = certBasePath + "cert.crt"
|
||||||
|
KeyFileDefault = certBasePath + "priv.key"
|
||||||
|
ACMEKeyFileDefault = certBasePath + "acme.key"
|
||||||
|
)
|
||||||
@@ -13,19 +13,17 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/certificate"
|
"github.com/go-acme/lego/v4/certificate"
|
||||||
"github.com/go-acme/lego/v4/challenge"
|
|
||||||
"github.com/go-acme/lego/v4/lego"
|
"github.com/go-acme/lego/v4/lego"
|
||||||
"github.com/go-acme/lego/v4/registration"
|
"github.com/go-acme/lego/v4/registration"
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Provider struct {
|
Provider struct {
|
||||||
cfg *AutocertConfig
|
cfg *Config
|
||||||
user *User
|
user *User
|
||||||
legoCfg *lego.Config
|
legoCfg *lego.Config
|
||||||
client *lego.Client
|
client *lego.Client
|
||||||
@@ -36,13 +34,20 @@ type (
|
|||||||
|
|
||||||
obtainMu sync.Mutex
|
obtainMu sync.Mutex
|
||||||
}
|
}
|
||||||
ProviderGenerator func(ProviderOpt) (challenge.Provider, gperr.Error)
|
|
||||||
|
|
||||||
CertExpiries map[string]time.Time
|
CertExpiries map[string]time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrGetCertFailure = errors.New("get certificate failed")
|
var ErrGetCertFailure = errors.New("get certificate failed")
|
||||||
|
|
||||||
|
func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) *Provider {
|
||||||
|
return &Provider{
|
||||||
|
cfg: cfg,
|
||||||
|
user: user,
|
||||||
|
legoCfg: legoCfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
if p.tlsCert == nil {
|
if p.tlsCert == nil {
|
||||||
return nil, ErrGetCertFailure
|
return nil, ErrGetCertFailure
|
||||||
@@ -205,7 +210,7 @@ func (p *Provider) initClient() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
generator := providersGenMap[p.cfg.Provider]
|
generator := Providers[p.cfg.Provider]
|
||||||
legoProvider, pErr := generator(p.cfg.Options)
|
legoProvider, pErr := generator(p.cfg.Options)
|
||||||
if pErr != nil {
|
if pErr != nil {
|
||||||
return pErr
|
return pErr
|
||||||
@@ -322,18 +327,3 @@ func getCertExpiries(cert *tls.Certificate) (CertExpiries, error) {
|
|||||||
}
|
}
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerGenerator[CT any, PT challenge.Provider](
|
|
||||||
defaultCfg func() *CT,
|
|
||||||
newProvider func(*CT) (PT, error),
|
|
||||||
) ProviderGenerator {
|
|
||||||
return func(opt ProviderOpt) (challenge.Provider, gperr.Error) {
|
|
||||||
cfg := defaultCfg()
|
|
||||||
err := U.Deserialize(opt, &cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p, pErr := newProvider(cfg)
|
|
||||||
return p, gperr.Wrap(pErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
"github.com/goccy/go-yaml"
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/yaml.v3"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// type Config struct {
|
// type Config struct {
|
||||||
@@ -44,7 +44,7 @@ oauth2_config:
|
|||||||
}
|
}
|
||||||
testYaml = testYaml[1:] // remove first \n
|
testYaml = testYaml[1:] // remove first \n
|
||||||
opt := make(map[string]any)
|
opt := make(map[string]any)
|
||||||
ExpectNoError(t, yaml.Unmarshal([]byte(testYaml), opt))
|
require.NoError(t, yaml.Unmarshal([]byte(testYaml), &opt))
|
||||||
ExpectNoError(t, U.Deserialize(opt, cfg))
|
require.NoError(t, utils.Deserialize(opt, cfg))
|
||||||
ExpectEqual(t, cfg, cfgExpected)
|
require.Equal(t, cfg, cfgExpected)
|
||||||
}
|
}
|
||||||
|
|||||||
26
internal/autocert/providers.go
Normal file
26
internal/autocert/providers.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package autocert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-acme/lego/v4/challenge"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Generator func(map[string]any) (challenge.Provider, gperr.Error)
|
||||||
|
|
||||||
|
var Providers = make(map[string]Generator)
|
||||||
|
|
||||||
|
func DNSProvider[CT any, PT challenge.Provider](
|
||||||
|
defaultCfg func() *CT,
|
||||||
|
newProvider func(*CT) (PT, error),
|
||||||
|
) Generator {
|
||||||
|
return func(opt map[string]any) (challenge.Provider, gperr.Error) {
|
||||||
|
cfg := defaultCfg()
|
||||||
|
err := utils.Deserialize(opt, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p, pErr := newProvider(cfg)
|
||||||
|
return p, gperr.Wrap(pErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
internal/autocert/types/provider.go
Normal file
14
internal/autocert/types/provider.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package autocert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
Setup() error
|
||||||
|
GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||||
|
ScheduleRenewal(task.Parent)
|
||||||
|
ObtainCert() error
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
type User struct {
|
type User struct {
|
||||||
Email string
|
Email string
|
||||||
Registration *registration.Resource
|
Registration *registration.Resource
|
||||||
key crypto.PrivateKey
|
Key crypto.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) GetEmail() string {
|
func (u *User) GetEmail() string {
|
||||||
@@ -21,5 +21,5 @@ func (u *User) GetRegistration() *registration.Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) GetPrivateKey() crypto.PrivateKey {
|
func (u *User) GetPrivateKey() crypto.PrivateKey {
|
||||||
return u.key
|
return u.Key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,22 +10,24 @@ const (
|
|||||||
DotEnvPath = ".env"
|
DotEnvPath = ".env"
|
||||||
DotEnvExamplePath = ".env.example"
|
DotEnvExamplePath = ".env.example"
|
||||||
|
|
||||||
ConfigBasePath = "config"
|
ConfigBasePath = "config"
|
||||||
ConfigFileName = "config.yml"
|
ConfigFileName = "config.yml"
|
||||||
ConfigExampleFileName = "config.example.yml"
|
ConfigExampleFileName = "config.example.yml"
|
||||||
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
||||||
HomepageJSONConfigPath = ConfigBasePath + "/.homepage.json"
|
|
||||||
IconListCachePath = ConfigBasePath + "/.icon_list_cache.json"
|
IconListCachePath = ConfigBasePath + "/.icon_list_cache.json"
|
||||||
IconCachePath = ConfigBasePath + "/.icon_cache.json"
|
|
||||||
|
NamespaceHomepageOverrides = ".homepage"
|
||||||
|
NamespaceIconCache = ".icon_cache"
|
||||||
|
|
||||||
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
|
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
|
||||||
|
|
||||||
ComposeFileName = "compose.yml"
|
ComposeFileName = "compose.yml"
|
||||||
ComposeExampleFileName = "compose.example.yml"
|
ComposeExampleFileName = "compose.example.yml"
|
||||||
|
|
||||||
ErrorPagesBasePath = "error_pages"
|
DataDir = "data"
|
||||||
|
|
||||||
AgentCertsBasePath = "certs"
|
ErrorPagesBasePath = "error_pages"
|
||||||
)
|
)
|
||||||
|
|
||||||
var RequiredDirectories = []string{
|
var RequiredDirectories = []string{
|
||||||
@@ -40,7 +42,7 @@ const (
|
|||||||
HealthCheckIntervalDefault = 5 * time.Second
|
HealthCheckIntervalDefault = 5 * time.Second
|
||||||
HealthCheckTimeoutDefault = 5 * time.Second
|
HealthCheckTimeoutDefault = 5 * time.Second
|
||||||
|
|
||||||
WakeTimeoutDefault = "30s"
|
WakeTimeoutDefault = "3m"
|
||||||
StopTimeoutDefault = "30s"
|
StopTimeoutDefault = "3m"
|
||||||
StopMethodDefault = "stop"
|
StopMethodDefault = "stop"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func decodeJWTKey(key string) []byte {
|
|||||||
}
|
}
|
||||||
bytes, err := base64.StdEncoding.DecodeString(key)
|
bytes, err := base64.StdEncoding.DecodeString(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic().Err(err).Msg("failed to decode jwt key")
|
log.Fatal().Str("key", key).Err(err).Msg("failed to decode secret")
|
||||||
}
|
}
|
||||||
return bytes
|
return bytes
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ func RandomJWTKey() []byte {
|
|||||||
key := make([]byte, 32)
|
key := make([]byte, 32)
|
||||||
_, err := rand.Read(key)
|
_, err := rand.Read(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic().Err(err).Msg("failed to generate random jwt key")
|
log.Fatal().Err(err).Msg("failed to generate random jwt key")
|
||||||
}
|
}
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ var (
|
|||||||
IsDebug = GetEnvBool("DEBUG", IsTest)
|
IsDebug = GetEnvBool("DEBUG", IsTest)
|
||||||
IsTrace = GetEnvBool("TRACE", false) && IsDebug
|
IsTrace = GetEnvBool("TRACE", false) && IsDebug
|
||||||
|
|
||||||
|
HTTP3Enabled = GetEnvBool("HTTP3_ENABLED", true)
|
||||||
|
|
||||||
ProxyHTTPAddr,
|
ProxyHTTPAddr,
|
||||||
ProxyHTTPHost,
|
ProxyHTTPHost,
|
||||||
ProxyHTTPPort,
|
ProxyHTTPPort,
|
||||||
@@ -34,11 +36,9 @@ var (
|
|||||||
APIHTTPPort,
|
APIHTTPPort,
|
||||||
APIHTTPURL = GetAddrEnv("API_ADDR", "127.0.0.1:8888", "http")
|
APIHTTPURL = GetAddrEnv("API_ADDR", "127.0.0.1:8888", "http")
|
||||||
|
|
||||||
PrometheusEnabled = GetEnvBool("PROMETHEUS_ENABLED", false)
|
|
||||||
|
|
||||||
APIJWTSecure = GetEnvBool("API_JWT_SECURE", true)
|
APIJWTSecure = GetEnvBool("API_JWT_SECURE", true)
|
||||||
APIJWTSecret = decodeJWTKey(GetEnvString("API_JWT_SECRET", ""))
|
APIJWTSecret = decodeJWTKey(GetEnvString("API_JWT_SECRET", ""))
|
||||||
APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", time.Hour)
|
APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", 24*time.Hour)
|
||||||
APIUser = GetEnvString("API_USER", "admin")
|
APIUser = GetEnvString("API_USER", "admin")
|
||||||
APIPassword = GetEnvString("API_PASSWORD", "password")
|
APIPassword = GetEnvString("API_PASSWORD", "password")
|
||||||
|
|
||||||
@@ -48,8 +48,7 @@ var (
|
|||||||
OIDCIssuerURL = GetEnvString("OIDC_ISSUER_URL", "")
|
OIDCIssuerURL = GetEnvString("OIDC_ISSUER_URL", "")
|
||||||
OIDCClientID = GetEnvString("OIDC_CLIENT_ID", "")
|
OIDCClientID = GetEnvString("OIDC_CLIENT_ID", "")
|
||||||
OIDCClientSecret = GetEnvString("OIDC_CLIENT_SECRET", "")
|
OIDCClientSecret = GetEnvString("OIDC_CLIENT_SECRET", "")
|
||||||
OIDCRedirectURL = GetEnvString("OIDC_REDIRECT_URL", "")
|
OIDCScopes = GetCommaSepEnv("OIDC_SCOPES", "openid, profile, email, groups")
|
||||||
OIDCScopes = GetEnvString("OIDC_SCOPES", "openid, profile, email")
|
|
||||||
OIDCAllowedUsers = GetCommaSepEnv("OIDC_ALLOWED_USERS", "")
|
OIDCAllowedUsers = GetCommaSepEnv("OIDC_ALLOWED_USERS", "")
|
||||||
OIDCAllowedGroups = GetCommaSepEnv("OIDC_ALLOWED_GROUPS", "")
|
OIDCAllowedGroups = GetCommaSepEnv("OIDC_ALLOWED_GROUPS", "")
|
||||||
|
|
||||||
@@ -59,6 +58,8 @@ var (
|
|||||||
MetricsDisableDisk = GetEnvBool("METRICS_DISABLE_DISK", false)
|
MetricsDisableDisk = GetEnvBool("METRICS_DISABLE_DISK", false)
|
||||||
MetricsDisableNetwork = GetEnvBool("METRICS_DISABLE_NETWORK", false)
|
MetricsDisableNetwork = GetEnvBool("METRICS_DISABLE_NETWORK", false)
|
||||||
MetricsDisableSensors = GetEnvBool("METRICS_DISABLE_SENSORS", false)
|
MetricsDisableSensors = GetEnvBool("METRICS_DISABLE_SENSORS", false)
|
||||||
|
|
||||||
|
ForceResolveCountry = GetEnvBool("FORCE_RESOLVE_COUNTRY", false)
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T {
|
func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/api"
|
"github.com/yusing/go-proxy/internal/api"
|
||||||
"github.com/yusing/go-proxy/internal/autocert"
|
autocert "github.com/yusing/go-proxy/internal/autocert"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
"github.com/yusing/go-proxy/internal/entrypoint"
|
"github.com/yusing/go-proxy/internal/entrypoint"
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/server"
|
"github.com/yusing/go-proxy/internal/net/gphttp/server"
|
||||||
"github.com/yusing/go-proxy/internal/notif"
|
"github.com/yusing/go-proxy/internal/notif"
|
||||||
|
"github.com/yusing/go-proxy/internal/proxmox"
|
||||||
proxy "github.com/yusing/go-proxy/internal/route/provider"
|
proxy "github.com/yusing/go-proxy/internal/route/provider"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
@@ -197,6 +198,7 @@ func (cfg *Config) StartServers(opts ...*StartServersOptions) {
|
|||||||
HTTPAddr: common.ProxyHTTPAddr,
|
HTTPAddr: common.ProxyHTTPAddr,
|
||||||
HTTPSAddr: common.ProxyHTTPSAddr,
|
HTTPSAddr: common.ProxyHTTPSAddr,
|
||||||
Handler: cfg.entrypoint,
|
Handler: cfg.entrypoint,
|
||||||
|
ACL: cfg.value.ACL,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if opt.API {
|
if opt.API {
|
||||||
@@ -228,6 +230,7 @@ func (cfg *Config) load() gperr.Error {
|
|||||||
errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
|
errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
|
||||||
cfg.initNotification(model.Providers.Notification)
|
cfg.initNotification(model.Providers.Notification)
|
||||||
errs.Add(cfg.initAutoCert(model.AutoCert))
|
errs.Add(cfg.initAutoCert(model.AutoCert))
|
||||||
|
errs.Add(cfg.initProxmox(model.Providers.Proxmox))
|
||||||
errs.Add(cfg.loadRouteProviders(&model.Providers))
|
errs.Add(cfg.loadRouteProviders(&model.Providers))
|
||||||
|
|
||||||
cfg.value = model
|
cfg.value = model
|
||||||
@@ -237,6 +240,14 @@ func (cfg *Config) load() gperr.Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cfg.entrypoint.SetFindRouteDomains(model.MatchDomains)
|
cfg.entrypoint.SetFindRouteDomains(model.MatchDomains)
|
||||||
|
if model.ACL.Valid() {
|
||||||
|
err := model.ACL.Start(cfg.task)
|
||||||
|
if err != nil {
|
||||||
|
errs.Add(err)
|
||||||
|
} else {
|
||||||
|
logging.Info().Msg("ACL started")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return errs.Error()
|
return errs.Error()
|
||||||
}
|
}
|
||||||
@@ -251,13 +262,33 @@ func (cfg *Config) initNotification(notifCfg []notif.NotificationConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) initAutoCert(autocertCfg *autocert.AutocertConfig) (err gperr.Error) {
|
func (cfg *Config) initAutoCert(autocertCfg *autocert.Config) gperr.Error {
|
||||||
if cfg.autocertProvider != nil {
|
if cfg.autocertProvider != nil {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.autocertProvider, err = autocertCfg.GetProvider()
|
if autocertCfg == nil {
|
||||||
return
|
autocertCfg = new(autocert.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, legoCfg, err := autocertCfg.GetLegoConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.autocertProvider = autocert.NewProvider(autocertCfg, user, legoCfg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) initProxmox(proxmoxCfg []proxmox.Config) gperr.Error {
|
||||||
|
proxmox.Clients.Clear()
|
||||||
|
var errs = gperr.NewBuilder()
|
||||||
|
for _, cfg := range proxmoxCfg {
|
||||||
|
if err := cfg.Init(); err != nil {
|
||||||
|
errs.Add(err.Subject(cfg.URL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) errIfExists(p *proxy.Provider) gperr.Error {
|
func (cfg *Config) errIfExists(p *proxy.Provider) gperr.Error {
|
||||||
@@ -319,6 +350,7 @@ func (cfg *Config) loadRouteProviders(providers *config.Providers) gperr.Error {
|
|||||||
lenLongestName = len(k)
|
lenLongestName = len(k)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
results.EnableConcurrency()
|
||||||
cfg.providers.RangeAllParallel(func(_ string, p *proxy.Provider) {
|
cfg.providers.RangeAllParallel(func(_ string, p *proxy.Provider) {
|
||||||
if err := p.LoadRoutes(); err != nil {
|
if err := p.LoadRoutes(); err != nil {
|
||||||
errs.Add(err.Subject(p.String()))
|
errs.Add(err.Subject(p.String()))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package types
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -7,31 +7,38 @@ import (
|
|||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/go-proxy/internal/acl"
|
||||||
"github.com/yusing/go-proxy/internal/autocert"
|
"github.com/yusing/go-proxy/internal/autocert"
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/accesslog"
|
"github.com/yusing/go-proxy/internal/logging/accesslog"
|
||||||
"github.com/yusing/go-proxy/internal/notif"
|
"github.com/yusing/go-proxy/internal/notif"
|
||||||
|
"github.com/yusing/go-proxy/internal/proxmox"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Config struct {
|
Config struct {
|
||||||
AutoCert *autocert.AutocertConfig `json:"autocert"`
|
ACL *acl.Config `json:"acl"`
|
||||||
Entrypoint Entrypoint `json:"entrypoint"`
|
AutoCert *autocert.Config `json:"autocert"`
|
||||||
Providers Providers `json:"providers"`
|
Entrypoint Entrypoint `json:"entrypoint"`
|
||||||
MatchDomains []string `json:"match_domains" validate:"domain_name"`
|
Providers Providers `json:"providers"`
|
||||||
Homepage HomepageConfig `json:"homepage"`
|
MatchDomains []string `json:"match_domains" validate:"domain_name"`
|
||||||
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
|
Homepage HomepageConfig `json:"homepage"`
|
||||||
|
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
|
||||||
}
|
}
|
||||||
Providers struct {
|
Providers struct {
|
||||||
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
|
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
|
||||||
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"`
|
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"`
|
||||||
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
|
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
|
||||||
Notification []notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
|
Notification []notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
|
||||||
|
Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"`
|
||||||
}
|
}
|
||||||
Entrypoint struct {
|
Entrypoint struct {
|
||||||
Middlewares []map[string]any `json:"middlewares"`
|
Middlewares []map[string]any `json:"middlewares"`
|
||||||
AccessLog *accesslog.Config `json:"access_log" validate:"omitempty"`
|
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
HomepageConfig struct {
|
||||||
|
UseDefaultCategories bool `json:"use_default_categories"`
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigInstance interface {
|
ConfigInstance interface {
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
type HomepageConfig struct {
|
|
||||||
UseDefaultCategories bool `json:"use_default_categories"`
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package autocert
|
package dnsproviders
|
||||||
|
|
||||||
type DummyConfig struct{}
|
type DummyConfig struct{}
|
||||||
type DummyProvider struct{}
|
type DummyProvider struct{}
|
||||||
55
internal/dnsproviders/gen.py
Normal file
55
internal/dnsproviders/gen.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
class Entry:
|
||||||
|
def __init__(self, name: str, type: str, **kwargs) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.type = type
|
||||||
|
|
||||||
|
url = "https://api.github.com/repos/go-acme/lego/contents/providers/dns"
|
||||||
|
response = requests.get(url)
|
||||||
|
data: list[Entry] = [Entry(**i) for i in response.json()]
|
||||||
|
|
||||||
|
header = "//go:generate /usr/bin/python3 gen.py\n\npackage dnsproviders\n\n"
|
||||||
|
names: list[str] = [
|
||||||
|
"Local = \"local\"",
|
||||||
|
"Pseudo = \"pseudo\"",
|
||||||
|
]
|
||||||
|
imports: list[str] = [
|
||||||
|
"\"github.com/yusing/go-proxy/internal/autocert\""
|
||||||
|
]
|
||||||
|
genMap: list[str] = [
|
||||||
|
"autocert.Providers[Local] = autocert.DNSProvider(NewDummyDefaultConfig, NewDummyDNSProviderConfig)",
|
||||||
|
"autocert.Providers[Pseudo] = autocert.DNSProvider(NewDummyDefaultConfig, NewDummyDNSProviderConfig)",
|
||||||
|
]
|
||||||
|
|
||||||
|
blacklists = [
|
||||||
|
"internal",
|
||||||
|
# deprecated
|
||||||
|
"azure",
|
||||||
|
"brandit",
|
||||||
|
"cloudxns",
|
||||||
|
"dnspod",
|
||||||
|
"mythicbeasts",
|
||||||
|
"yandexcloud"
|
||||||
|
]
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
if item.type != "dir" or item.name in blacklists:
|
||||||
|
continue
|
||||||
|
imports.append(f"\"github.com/go-acme/lego/v4/providers/dns/{item.name}\"")
|
||||||
|
genMap.append(f"autocert.Providers[\"{item.name}\"] = autocert.DNSProvider({item.name}.NewDefaultConfig, {item.name}.NewDNSProviderConfig)")
|
||||||
|
|
||||||
|
with open("providers.go", "w") as f:
|
||||||
|
f.write(header)
|
||||||
|
f.write("import (\n")
|
||||||
|
f.write("\n".join(imports))
|
||||||
|
f.write("\n)\n\n")
|
||||||
|
f.write("const (\n")
|
||||||
|
f.write("\n".join(names))
|
||||||
|
f.write("\n)\n\n")
|
||||||
|
f.write("func InitProviders() {\n")
|
||||||
|
f.write("\n".join(genMap))
|
||||||
|
f.write("\n}\n\n")
|
||||||
|
|
||||||
|
os.execvp("go", ["go", "fmt", "providers.go"])
|
||||||
194
internal/dnsproviders/go.mod
Normal file
194
internal/dnsproviders/go.mod
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
module github.com/yusing/go-proxy/internal/dnsproviders
|
||||||
|
|
||||||
|
go 1.24.2
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy => ../..
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-acme/lego/v4 v4.23.1
|
||||||
|
github.com/yusing/go-proxy v0.0.0-00010101000000-000000000000
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/auth v0.16.1 // indirect
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||||
|
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||||
|
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||||
|
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
||||||
|
github.com/aliyun/alibaba-cloud-sdk-go v1.63.106 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.2 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.51.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||||
|
github.com/aws/smithy-go v1.22.3 // indirect
|
||||||
|
github.com/baidubce/bce-sdk-go v0.9.224 // indirect
|
||||||
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
|
github.com/boombuler/barcode v1.0.2 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/civo/civogo v0.3.98 // indirect
|
||||||
|
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||||
|
github.com/exoscale/egoscale/v3 v3.1.14 // indirect
|
||||||
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
|
github.com/go-errors/errors v1.5.1 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
|
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.17.1 // indirect
|
||||||
|
github.com/gofrs/flock v0.12.1 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||||
|
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
||||||
|
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||||
|
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||||
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.146 // indirect
|
||||||
|
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||||
|
github.com/infobloxopen/infoblox-go-client/v2 v2.9.0 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
||||||
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||||
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
|
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||||
|
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/linode/linodego v1.49.0 // indirect
|
||||||
|
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
|
||||||
|
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/miekg/dns v1.1.65 // indirect
|
||||||
|
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||||
|
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||||
|
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
|
||||||
|
github.com/nrdcg/desec v0.11.0 // indirect
|
||||||
|
github.com/nrdcg/freemyip v0.3.0 // indirect
|
||||||
|
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||||
|
github.com/nrdcg/goinwx v0.11.0 // indirect
|
||||||
|
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||||
|
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||||
|
github.com/nrdcg/nodion v0.1.0 // indirect
|
||||||
|
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||||
|
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
|
github.com/oracle/oci-go-sdk/v65 v65.89.2 // indirect
|
||||||
|
github.com/ovh/go-ovh v1.7.0 // indirect
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/peterhellberg/link v1.2.0 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/pquerna/otp v1.4.0 // indirect
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||||
|
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
|
||||||
|
github.com/rs/zerolog v1.34.0 // indirect
|
||||||
|
github.com/sacloud/api-client-go v0.2.10 // indirect
|
||||||
|
github.com/sacloud/go-http v0.1.9 // indirect
|
||||||
|
github.com/sacloud/iaas-api-go v1.14.0 // indirect
|
||||||
|
github.com/sacloud/packages-go v0.0.11 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 // indirect
|
||||||
|
github.com/selectel/domains-go v1.1.0 // indirect
|
||||||
|
github.com/selectel/go-selvpcclient/v3 v3.2.1 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||||
|
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||||
|
github.com/softlayer/softlayer-go v1.1.7 // indirect
|
||||||
|
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||||
|
github.com/sony/gobreaker v1.0.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
|
github.com/spf13/cast v1.7.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
|
github.com/spf13/viper v1.20.1 // indirect
|
||||||
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1150 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 // indirect
|
||||||
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
|
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||||
|
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
||||||
|
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||||
|
github.com/volcengine/volc-sdk-golang v1.0.205 // indirect
|
||||||
|
github.com/vultr/govultr/v3 v3.19.1 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/ratelimit v0.3.1 // indirect
|
||||||
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
|
golang.org/x/net v0.39.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.29.0 // indirect
|
||||||
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
|
golang.org/x/text v0.24.0 // indirect
|
||||||
|
golang.org/x/time v0.11.0 // indirect
|
||||||
|
golang.org/x/tools v0.32.0 // indirect
|
||||||
|
google.golang.org/api v0.230.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect
|
||||||
|
google.golang.org/grpc v1.72.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/ns1/ns1-go.v2 v2.14.2 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
k8s.io/api v0.33.0 // indirect
|
||||||
|
k8s.io/apimachinery v0.33.0 // indirect
|
||||||
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
|
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
|
||||||
|
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||||
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
|
)
|
||||||
2485
internal/dnsproviders/go.sum
Normal file
2485
internal/dnsproviders/go.sum
Normal file
File diff suppressed because it is too large
Load Diff
309
internal/dnsproviders/providers.go
Normal file
309
internal/dnsproviders/providers.go
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
//go:generate /usr/bin/python3 gen.py
|
||||||
|
|
||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/acmedns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/active24"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/allinkl"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/arvancloud"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/auroradns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/autodns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/axelname"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/azuredns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/baiducloud"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/bindman"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/bluecat"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/bookmyname"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/bunny"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/checkdomain"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/civo"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cloudns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cloudru"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/conoha"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/constellix"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/corenetworks"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/cpanel"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/derak"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/desec"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/designate"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/digitalocean"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/directadmin"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/dnshomede"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/dnsimple"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/dode"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/domeneshop"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/dreamhost"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/dyn"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/dynu"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/easydns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/edgedns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/efficientip"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/epik"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/exec"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/exoscale"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/f5xc"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/freemyip"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/gandi"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/gandiv5"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/gcloud"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/gcore"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/glesys"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/googledomains"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/hetzner"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/hostingde"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/hosttech"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/httpnet"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/httpreq"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/hurricane"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/hyperone"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/ibmcloud"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/iij"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/iijdpf"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/infoblox"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/infomaniak"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/internetbs"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/inwx"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/ionos"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/ipv64"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/iwantmyname"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/joker"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/liara"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/lightsail"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/limacity"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/linode"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/liquidweb"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/loopia"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/luadns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/mailinabox"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/manageengine"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/metaname"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/metaregistrar"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/mijnhost"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/mittwald"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/myaddr"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/mydnsjp"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/namecheap"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/namedotcom"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/netcup"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/netlify"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/nicmanager"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/nifcloud"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/njalla"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/nodion"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/ns1"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/oraclecloud"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/otc"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/pdns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/plesk"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/porkbun"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/rackspace"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/rainyun"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/rcodezero"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/regfish"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/regru"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/rfc2136"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/rimuhosting"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/route53"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/safedns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/sakuracloud"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/scaleway"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/selectel"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/selectelv2"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/selfhostde"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/servercow"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/shellrent"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/simply"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/sonic"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/spaceship"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/stackpath"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/technitium"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/timewebcloud"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/transip"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/ultradns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/variomedia"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/vegadns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/vercel"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/versio"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/vinyldns"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/vkcloud"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/volcengine"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/vscale"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/vultr"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/webnames"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/websupport"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/wedos"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/westcn"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/yandex"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/yandex360"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/zoneee"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/zonomi"
|
||||||
|
"github.com/yusing/go-proxy/internal/autocert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Local = "local"
|
||||||
|
Pseudo = "pseudo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitProviders() {
|
||||||
|
autocert.Providers[Local] = autocert.DNSProvider(NewDummyDefaultConfig, NewDummyDNSProviderConfig)
|
||||||
|
autocert.Providers[Pseudo] = autocert.DNSProvider(NewDummyDefaultConfig, NewDummyDNSProviderConfig)
|
||||||
|
autocert.Providers["acmedns"] = autocert.DNSProvider(acmedns.NewDefaultConfig, acmedns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["active24"] = autocert.DNSProvider(active24.NewDefaultConfig, active24.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["alidns"] = autocert.DNSProvider(alidns.NewDefaultConfig, alidns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["allinkl"] = autocert.DNSProvider(allinkl.NewDefaultConfig, allinkl.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["arvancloud"] = autocert.DNSProvider(arvancloud.NewDefaultConfig, arvancloud.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["auroradns"] = autocert.DNSProvider(auroradns.NewDefaultConfig, auroradns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["autodns"] = autocert.DNSProvider(autodns.NewDefaultConfig, autodns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["axelname"] = autocert.DNSProvider(axelname.NewDefaultConfig, axelname.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["azuredns"] = autocert.DNSProvider(azuredns.NewDefaultConfig, azuredns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["baiducloud"] = autocert.DNSProvider(baiducloud.NewDefaultConfig, baiducloud.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["bindman"] = autocert.DNSProvider(bindman.NewDefaultConfig, bindman.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["bluecat"] = autocert.DNSProvider(bluecat.NewDefaultConfig, bluecat.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["bookmyname"] = autocert.DNSProvider(bookmyname.NewDefaultConfig, bookmyname.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["bunny"] = autocert.DNSProvider(bunny.NewDefaultConfig, bunny.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["checkdomain"] = autocert.DNSProvider(checkdomain.NewDefaultConfig, checkdomain.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["civo"] = autocert.DNSProvider(civo.NewDefaultConfig, civo.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["clouddns"] = autocert.DNSProvider(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["cloudflare"] = autocert.DNSProvider(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["cloudns"] = autocert.DNSProvider(cloudns.NewDefaultConfig, cloudns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["cloudru"] = autocert.DNSProvider(cloudru.NewDefaultConfig, cloudru.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["conoha"] = autocert.DNSProvider(conoha.NewDefaultConfig, conoha.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["constellix"] = autocert.DNSProvider(constellix.NewDefaultConfig, constellix.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["corenetworks"] = autocert.DNSProvider(corenetworks.NewDefaultConfig, corenetworks.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["cpanel"] = autocert.DNSProvider(cpanel.NewDefaultConfig, cpanel.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["derak"] = autocert.DNSProvider(derak.NewDefaultConfig, derak.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["desec"] = autocert.DNSProvider(desec.NewDefaultConfig, desec.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["designate"] = autocert.DNSProvider(designate.NewDefaultConfig, designate.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["digitalocean"] = autocert.DNSProvider(digitalocean.NewDefaultConfig, digitalocean.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["directadmin"] = autocert.DNSProvider(directadmin.NewDefaultConfig, directadmin.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["dnshomede"] = autocert.DNSProvider(dnshomede.NewDefaultConfig, dnshomede.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["dnsimple"] = autocert.DNSProvider(dnsimple.NewDefaultConfig, dnsimple.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["dnsmadeeasy"] = autocert.DNSProvider(dnsmadeeasy.NewDefaultConfig, dnsmadeeasy.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["dode"] = autocert.DNSProvider(dode.NewDefaultConfig, dode.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["domeneshop"] = autocert.DNSProvider(domeneshop.NewDefaultConfig, domeneshop.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["dreamhost"] = autocert.DNSProvider(dreamhost.NewDefaultConfig, dreamhost.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["duckdns"] = autocert.DNSProvider(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["dyn"] = autocert.DNSProvider(dyn.NewDefaultConfig, dyn.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["dynu"] = autocert.DNSProvider(dynu.NewDefaultConfig, dynu.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["easydns"] = autocert.DNSProvider(easydns.NewDefaultConfig, easydns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["edgedns"] = autocert.DNSProvider(edgedns.NewDefaultConfig, edgedns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["efficientip"] = autocert.DNSProvider(efficientip.NewDefaultConfig, efficientip.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["epik"] = autocert.DNSProvider(epik.NewDefaultConfig, epik.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["exec"] = autocert.DNSProvider(exec.NewDefaultConfig, exec.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["exoscale"] = autocert.DNSProvider(exoscale.NewDefaultConfig, exoscale.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["f5xc"] = autocert.DNSProvider(f5xc.NewDefaultConfig, f5xc.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["freemyip"] = autocert.DNSProvider(freemyip.NewDefaultConfig, freemyip.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["gandi"] = autocert.DNSProvider(gandi.NewDefaultConfig, gandi.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["gandiv5"] = autocert.DNSProvider(gandiv5.NewDefaultConfig, gandiv5.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["gcloud"] = autocert.DNSProvider(gcloud.NewDefaultConfig, gcloud.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["gcore"] = autocert.DNSProvider(gcore.NewDefaultConfig, gcore.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["glesys"] = autocert.DNSProvider(glesys.NewDefaultConfig, glesys.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["godaddy"] = autocert.DNSProvider(godaddy.NewDefaultConfig, godaddy.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["googledomains"] = autocert.DNSProvider(googledomains.NewDefaultConfig, googledomains.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["hetzner"] = autocert.DNSProvider(hetzner.NewDefaultConfig, hetzner.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["hostingde"] = autocert.DNSProvider(hostingde.NewDefaultConfig, hostingde.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["hosttech"] = autocert.DNSProvider(hosttech.NewDefaultConfig, hosttech.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["httpnet"] = autocert.DNSProvider(httpnet.NewDefaultConfig, httpnet.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["httpreq"] = autocert.DNSProvider(httpreq.NewDefaultConfig, httpreq.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["huaweicloud"] = autocert.DNSProvider(huaweicloud.NewDefaultConfig, huaweicloud.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["hurricane"] = autocert.DNSProvider(hurricane.NewDefaultConfig, hurricane.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["hyperone"] = autocert.DNSProvider(hyperone.NewDefaultConfig, hyperone.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["ibmcloud"] = autocert.DNSProvider(ibmcloud.NewDefaultConfig, ibmcloud.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["iij"] = autocert.DNSProvider(iij.NewDefaultConfig, iij.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["iijdpf"] = autocert.DNSProvider(iijdpf.NewDefaultConfig, iijdpf.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["infoblox"] = autocert.DNSProvider(infoblox.NewDefaultConfig, infoblox.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["infomaniak"] = autocert.DNSProvider(infomaniak.NewDefaultConfig, infomaniak.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["internetbs"] = autocert.DNSProvider(internetbs.NewDefaultConfig, internetbs.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["inwx"] = autocert.DNSProvider(inwx.NewDefaultConfig, inwx.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["ionos"] = autocert.DNSProvider(ionos.NewDefaultConfig, ionos.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["ipv64"] = autocert.DNSProvider(ipv64.NewDefaultConfig, ipv64.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["iwantmyname"] = autocert.DNSProvider(iwantmyname.NewDefaultConfig, iwantmyname.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["joker"] = autocert.DNSProvider(joker.NewDefaultConfig, joker.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["liara"] = autocert.DNSProvider(liara.NewDefaultConfig, liara.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["lightsail"] = autocert.DNSProvider(lightsail.NewDefaultConfig, lightsail.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["limacity"] = autocert.DNSProvider(limacity.NewDefaultConfig, limacity.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["linode"] = autocert.DNSProvider(linode.NewDefaultConfig, linode.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["liquidweb"] = autocert.DNSProvider(liquidweb.NewDefaultConfig, liquidweb.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["loopia"] = autocert.DNSProvider(loopia.NewDefaultConfig, loopia.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["luadns"] = autocert.DNSProvider(luadns.NewDefaultConfig, luadns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["mailinabox"] = autocert.DNSProvider(mailinabox.NewDefaultConfig, mailinabox.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["manageengine"] = autocert.DNSProvider(manageengine.NewDefaultConfig, manageengine.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["metaname"] = autocert.DNSProvider(metaname.NewDefaultConfig, metaname.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["metaregistrar"] = autocert.DNSProvider(metaregistrar.NewDefaultConfig, metaregistrar.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["mijnhost"] = autocert.DNSProvider(mijnhost.NewDefaultConfig, mijnhost.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["mittwald"] = autocert.DNSProvider(mittwald.NewDefaultConfig, mittwald.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["myaddr"] = autocert.DNSProvider(myaddr.NewDefaultConfig, myaddr.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["mydnsjp"] = autocert.DNSProvider(mydnsjp.NewDefaultConfig, mydnsjp.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["namecheap"] = autocert.DNSProvider(namecheap.NewDefaultConfig, namecheap.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["namedotcom"] = autocert.DNSProvider(namedotcom.NewDefaultConfig, namedotcom.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["namesilo"] = autocert.DNSProvider(namesilo.NewDefaultConfig, namesilo.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["nearlyfreespeech"] = autocert.DNSProvider(nearlyfreespeech.NewDefaultConfig, nearlyfreespeech.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["netcup"] = autocert.DNSProvider(netcup.NewDefaultConfig, netcup.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["netlify"] = autocert.DNSProvider(netlify.NewDefaultConfig, netlify.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["nicmanager"] = autocert.DNSProvider(nicmanager.NewDefaultConfig, nicmanager.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["nifcloud"] = autocert.DNSProvider(nifcloud.NewDefaultConfig, nifcloud.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["njalla"] = autocert.DNSProvider(njalla.NewDefaultConfig, njalla.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["nodion"] = autocert.DNSProvider(nodion.NewDefaultConfig, nodion.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["ns1"] = autocert.DNSProvider(ns1.NewDefaultConfig, ns1.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["oraclecloud"] = autocert.DNSProvider(oraclecloud.NewDefaultConfig, oraclecloud.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["otc"] = autocert.DNSProvider(otc.NewDefaultConfig, otc.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["ovh"] = autocert.DNSProvider(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["pdns"] = autocert.DNSProvider(pdns.NewDefaultConfig, pdns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["plesk"] = autocert.DNSProvider(plesk.NewDefaultConfig, plesk.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["porkbun"] = autocert.DNSProvider(porkbun.NewDefaultConfig, porkbun.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["rackspace"] = autocert.DNSProvider(rackspace.NewDefaultConfig, rackspace.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["rainyun"] = autocert.DNSProvider(rainyun.NewDefaultConfig, rainyun.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["rcodezero"] = autocert.DNSProvider(rcodezero.NewDefaultConfig, rcodezero.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["regfish"] = autocert.DNSProvider(regfish.NewDefaultConfig, regfish.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["regru"] = autocert.DNSProvider(regru.NewDefaultConfig, regru.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["rfc2136"] = autocert.DNSProvider(rfc2136.NewDefaultConfig, rfc2136.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["rimuhosting"] = autocert.DNSProvider(rimuhosting.NewDefaultConfig, rimuhosting.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["route53"] = autocert.DNSProvider(route53.NewDefaultConfig, route53.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["safedns"] = autocert.DNSProvider(safedns.NewDefaultConfig, safedns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["sakuracloud"] = autocert.DNSProvider(sakuracloud.NewDefaultConfig, sakuracloud.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["scaleway"] = autocert.DNSProvider(scaleway.NewDefaultConfig, scaleway.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["selectel"] = autocert.DNSProvider(selectel.NewDefaultConfig, selectel.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["selectelv2"] = autocert.DNSProvider(selectelv2.NewDefaultConfig, selectelv2.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["selfhostde"] = autocert.DNSProvider(selfhostde.NewDefaultConfig, selfhostde.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["servercow"] = autocert.DNSProvider(servercow.NewDefaultConfig, servercow.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["shellrent"] = autocert.DNSProvider(shellrent.NewDefaultConfig, shellrent.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["simply"] = autocert.DNSProvider(simply.NewDefaultConfig, simply.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["sonic"] = autocert.DNSProvider(sonic.NewDefaultConfig, sonic.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["spaceship"] = autocert.DNSProvider(spaceship.NewDefaultConfig, spaceship.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["stackpath"] = autocert.DNSProvider(stackpath.NewDefaultConfig, stackpath.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["technitium"] = autocert.DNSProvider(technitium.NewDefaultConfig, technitium.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["tencentcloud"] = autocert.DNSProvider(tencentcloud.NewDefaultConfig, tencentcloud.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["timewebcloud"] = autocert.DNSProvider(timewebcloud.NewDefaultConfig, timewebcloud.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["transip"] = autocert.DNSProvider(transip.NewDefaultConfig, transip.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["ultradns"] = autocert.DNSProvider(ultradns.NewDefaultConfig, ultradns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["variomedia"] = autocert.DNSProvider(variomedia.NewDefaultConfig, variomedia.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["vegadns"] = autocert.DNSProvider(vegadns.NewDefaultConfig, vegadns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["vercel"] = autocert.DNSProvider(vercel.NewDefaultConfig, vercel.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["versio"] = autocert.DNSProvider(versio.NewDefaultConfig, versio.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["vinyldns"] = autocert.DNSProvider(vinyldns.NewDefaultConfig, vinyldns.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["vkcloud"] = autocert.DNSProvider(vkcloud.NewDefaultConfig, vkcloud.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["volcengine"] = autocert.DNSProvider(volcengine.NewDefaultConfig, volcengine.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["vscale"] = autocert.DNSProvider(vscale.NewDefaultConfig, vscale.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["vultr"] = autocert.DNSProvider(vultr.NewDefaultConfig, vultr.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["webnames"] = autocert.DNSProvider(webnames.NewDefaultConfig, webnames.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["websupport"] = autocert.DNSProvider(websupport.NewDefaultConfig, websupport.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["wedos"] = autocert.DNSProvider(wedos.NewDefaultConfig, wedos.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["westcn"] = autocert.DNSProvider(westcn.NewDefaultConfig, westcn.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["yandex"] = autocert.DNSProvider(yandex.NewDefaultConfig, yandex.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["yandex360"] = autocert.DNSProvider(yandex360.NewDefaultConfig, yandex360.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["zoneee"] = autocert.DNSProvider(zoneee.NewDefaultConfig, zoneee.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["zonomi"] = autocert.DNSProvider(zonomi.NewDefaultConfig, zonomi.NewDNSProviderConfig)
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -23,10 +24,10 @@ type (
|
|||||||
SharedClient struct {
|
SharedClient struct {
|
||||||
*client.Client
|
*client.Client
|
||||||
|
|
||||||
key string
|
|
||||||
refCount uint32
|
refCount uint32
|
||||||
closedOn int64
|
closedOn int64
|
||||||
|
|
||||||
|
key string
|
||||||
addr string
|
addr string
|
||||||
dial func(ctx context.Context) (net.Conn, error)
|
dial func(ctx context.Context) (net.Conn, error)
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func initClientCleaner() {
|
func initClientCleaner() {
|
||||||
cleaner := task.RootTask("docker_clients_cleaner")
|
cleaner := task.RootTask("docker_clients_cleaner", false)
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(cleanInterval)
|
ticker := time.NewTicker(cleanInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
@@ -66,7 +67,7 @@ func initClientCleaner() {
|
|||||||
defer clientMapMu.Unlock()
|
defer clientMapMu.Unlock()
|
||||||
|
|
||||||
for _, c := range clientMap {
|
for _, c := range clientMap {
|
||||||
delete(clientMap, c.key)
|
delete(clientMap, c.Key())
|
||||||
c.Client.Close()
|
c.Client.Close()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -80,30 +81,20 @@ func closeTimedOutClients() {
|
|||||||
|
|
||||||
for _, c := range clientMap {
|
for _, c := range clientMap {
|
||||||
if atomic.LoadUint32(&c.refCount) == 0 && now-atomic.LoadInt64(&c.closedOn) > clientTTLSecs {
|
if atomic.LoadUint32(&c.refCount) == 0 && now-atomic.LoadInt64(&c.closedOn) > clientTTLSecs {
|
||||||
delete(clientMap, c.key)
|
delete(clientMap, c.Key())
|
||||||
c.Client.Close()
|
c.Client.Close()
|
||||||
logging.Debug().Str("host", c.key).Msg("docker client closed")
|
logging.Debug().Str("host", c.DaemonHost()).Msg("docker client closed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SharedClient) Address() string {
|
func Clients() map[string]*SharedClient {
|
||||||
return c.addr
|
clientMapMu.RLock()
|
||||||
}
|
defer clientMapMu.RUnlock()
|
||||||
|
|
||||||
func (c *SharedClient) CheckConnection(ctx context.Context) error {
|
clients := make(map[string]*SharedClient, len(clientMap))
|
||||||
conn, err := c.dial(ctx)
|
maps.Copy(clients, clientMap)
|
||||||
if err != nil {
|
return clients
|
||||||
return err
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the client is still referenced, this is no-op.
|
|
||||||
func (c *SharedClient) Close() {
|
|
||||||
atomic.StoreInt64(&c.closedOn, time.Now().Unix())
|
|
||||||
atomic.AddUint32(&c.refCount, ^uint32(0))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new Docker client connection to the specified host.
|
// NewClient creates a new Docker client connection to the specified host.
|
||||||
@@ -187,9 +178,9 @@ func NewClient(host string) (*SharedClient, error) {
|
|||||||
|
|
||||||
c := &SharedClient{
|
c := &SharedClient{
|
||||||
Client: client,
|
Client: client,
|
||||||
key: host,
|
|
||||||
refCount: 1,
|
refCount: 1,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
|
key: host,
|
||||||
dial: dial,
|
dial: dial,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,9 +188,35 @@ func NewClient(host string) (*SharedClient, error) {
|
|||||||
if c.dial == nil {
|
if c.dial == nil {
|
||||||
c.dial = client.Dialer()
|
c.dial = client.Dialer()
|
||||||
}
|
}
|
||||||
|
if c.addr == "" {
|
||||||
|
c.addr = c.Client.DaemonHost()
|
||||||
|
}
|
||||||
|
|
||||||
defer logging.Debug().Str("host", host).Msg("docker client initialized")
|
defer logging.Debug().Str("host", host).Msg("docker client initialized")
|
||||||
|
|
||||||
clientMap[c.key] = c
|
clientMap[c.Key()] = c
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *SharedClient) Key() string {
|
||||||
|
return c.key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SharedClient) Address() string {
|
||||||
|
return c.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SharedClient) CheckConnection(ctx context.Context) error {
|
||||||
|
conn, err := c.dial(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the client is still referenced, this is no-op.
|
||||||
|
func (c *SharedClient) Close() {
|
||||||
|
atomic.StoreInt64(&c.closedOn, time.Now().Unix())
|
||||||
|
atomic.AddUint32(&c.refCount, ^uint32(0))
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/go-connections/nat"
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
idlewatcher "github.com/yusing/go-proxy/internal/idlewatcher/types"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -26,7 +27,8 @@ type (
|
|||||||
|
|
||||||
Agent *agent.AgentConfig `json:"agent"`
|
Agent *agent.AgentConfig `json:"agent"`
|
||||||
|
|
||||||
Labels map[string]string `json:"-"`
|
Labels map[string]string `json:"-"`
|
||||||
|
IdlewatcherConfig *idlewatcher.Config `json:"idlewatcher_config"`
|
||||||
|
|
||||||
Mounts []string `json:"mounts"`
|
Mounts []string `json:"mounts"`
|
||||||
|
|
||||||
@@ -35,16 +37,10 @@ type (
|
|||||||
PublicHostname string `json:"public_hostname"`
|
PublicHostname string `json:"public_hostname"`
|
||||||
PrivateHostname string `json:"private_hostname"`
|
PrivateHostname string `json:"private_hostname"`
|
||||||
|
|
||||||
Aliases []string `json:"aliases"`
|
Aliases []string `json:"aliases"`
|
||||||
IsExcluded bool `json:"is_excluded"`
|
IsExcluded bool `json:"is_excluded"`
|
||||||
IsExplicit bool `json:"is_explicit"`
|
IsExplicit bool `json:"is_explicit"`
|
||||||
IdleTimeout string `json:"idle_timeout,omitempty"`
|
Running bool `json:"running"`
|
||||||
WakeTimeout string `json:"wake_timeout,omitempty"`
|
|
||||||
StopMethod string `json:"stop_method,omitempty"`
|
|
||||||
StopTimeout string `json:"stop_timeout,omitempty"` // stop_method = "stop" only
|
|
||||||
StopSignal string `json:"stop_signal,omitempty"` // stop_method = "stop" | "kill" only
|
|
||||||
StartEndpoint string `json:"start_endpoint,omitempty"`
|
|
||||||
Running bool `json:"running"`
|
|
||||||
}
|
}
|
||||||
ContainerImage struct {
|
ContainerImage struct {
|
||||||
Author string `json:"author,omitempty"`
|
Author string `json:"author,omitempty"`
|
||||||
@@ -55,7 +51,7 @@ type (
|
|||||||
|
|
||||||
var DummyContainer = new(Container)
|
var DummyContainer = new(Container)
|
||||||
|
|
||||||
func FromDocker(c *container.Summary, dockerHost string) (res *Container) {
|
func FromDocker(c *container.SummaryTrimmed, dockerHost string) (res *Container) {
|
||||||
isExplicit := false
|
isExplicit := false
|
||||||
helper := containerHelper{c}
|
helper := containerHelper{c}
|
||||||
for lbl := range c.Labels {
|
for lbl := range c.Labels {
|
||||||
@@ -65,6 +61,8 @@ func FromDocker(c *container.Summary, dockerHost string) (res *Container) {
|
|||||||
delete(c.Labels, lbl)
|
delete(c.Labels, lbl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isExcluded, _ := strconv.ParseBool(helper.getDeleteLabel(LabelExclude))
|
||||||
res = &Container{
|
res = &Container{
|
||||||
DockerHost: dockerHost,
|
DockerHost: dockerHost,
|
||||||
Image: helper.parseImage(),
|
Image: helper.parseImage(),
|
||||||
@@ -78,16 +76,10 @@ func FromDocker(c *container.Summary, dockerHost string) (res *Container) {
|
|||||||
PublicPortMapping: helper.getPublicPortMapping(),
|
PublicPortMapping: helper.getPublicPortMapping(),
|
||||||
PrivatePortMapping: helper.getPrivatePortMapping(),
|
PrivatePortMapping: helper.getPrivatePortMapping(),
|
||||||
|
|
||||||
Aliases: helper.getAliases(),
|
Aliases: helper.getAliases(),
|
||||||
IsExcluded: strutils.ParseBool(helper.getDeleteLabel(LabelExclude)),
|
IsExcluded: isExcluded,
|
||||||
IsExplicit: isExplicit,
|
IsExplicit: isExplicit,
|
||||||
IdleTimeout: helper.getDeleteLabel(LabelIdleTimeout),
|
Running: c.Status == "running" || c.State == "running",
|
||||||
WakeTimeout: helper.getDeleteLabel(LabelWakeTimeout),
|
|
||||||
StopMethod: helper.getDeleteLabel(LabelStopMethod),
|
|
||||||
StopTimeout: helper.getDeleteLabel(LabelStopTimeout),
|
|
||||||
StopSignal: helper.getDeleteLabel(LabelStopSignal),
|
|
||||||
StartEndpoint: helper.getDeleteLabel(LabelStartEndpoint),
|
|
||||||
Running: c.Status == "running" || c.State == "running",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if agent.IsDockerHostAgent(dockerHost) {
|
if agent.IsDockerHostAgent(dockerHost) {
|
||||||
@@ -100,44 +92,10 @@ func FromDocker(c *container.Summary, dockerHost string) (res *Container) {
|
|||||||
|
|
||||||
res.setPrivateHostname(helper)
|
res.setPrivateHostname(helper)
|
||||||
res.setPublicHostname()
|
res.setPublicHostname()
|
||||||
|
res.loadDeleteIdlewatcherLabels(helper)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromInspectResponse(json container.InspectResponse, dockerHost string) *Container {
|
|
||||||
ports := make([]container.Port, 0)
|
|
||||||
for k, bindings := range json.NetworkSettings.Ports {
|
|
||||||
proto, privPortStr := nat.SplitProtoPort(string(k))
|
|
||||||
privPort, _ := strconv.ParseUint(privPortStr, 10, 16)
|
|
||||||
ports = append(ports, container.Port{
|
|
||||||
PrivatePort: uint16(privPort),
|
|
||||||
Type: proto,
|
|
||||||
})
|
|
||||||
for _, v := range bindings {
|
|
||||||
pubPort, _ := strconv.ParseUint(v.HostPort, 10, 16)
|
|
||||||
ports = append(ports, container.Port{
|
|
||||||
IP: v.HostIP,
|
|
||||||
PublicPort: uint16(pubPort),
|
|
||||||
PrivatePort: uint16(privPort),
|
|
||||||
Type: proto,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cont := FromDocker(&container.Summary{
|
|
||||||
ID: json.ID,
|
|
||||||
Names: []string{strings.TrimPrefix(json.Name, "/")},
|
|
||||||
Image: json.Image,
|
|
||||||
Ports: ports,
|
|
||||||
Labels: json.Config.Labels,
|
|
||||||
State: json.State.Status,
|
|
||||||
Status: json.State.Status,
|
|
||||||
Mounts: json.Mounts,
|
|
||||||
NetworkSettings: &container.NetworkSettingsSummary{
|
|
||||||
Networks: json.NetworkSettings.Networks,
|
|
||||||
},
|
|
||||||
}, dockerHost)
|
|
||||||
return cont
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Container) IsBlacklisted() bool {
|
func (c *Container) IsBlacklisted() bool {
|
||||||
return c.Image.IsBlacklisted() || c.isDatabase()
|
return c.Image.IsBlacklisted() || c.isDatabase()
|
||||||
}
|
}
|
||||||
@@ -200,3 +158,31 @@ func (c *Container) setPrivateHostname(helper containerHelper) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) loadDeleteIdlewatcherLabels(helper containerHelper) {
|
||||||
|
cfg := map[string]any{
|
||||||
|
"idle_timeout": helper.getDeleteLabel(LabelIdleTimeout),
|
||||||
|
"wake_timeout": helper.getDeleteLabel(LabelWakeTimeout),
|
||||||
|
"stop_method": helper.getDeleteLabel(LabelStopMethod),
|
||||||
|
"stop_timeout": helper.getDeleteLabel(LabelStopTimeout),
|
||||||
|
"stop_signal": helper.getDeleteLabel(LabelStopSignal),
|
||||||
|
"start_endpoint": helper.getDeleteLabel(LabelStartEndpoint),
|
||||||
|
}
|
||||||
|
// set only if idlewatcher is enabled
|
||||||
|
idleTimeout := cfg["idle_timeout"]
|
||||||
|
if idleTimeout != "" {
|
||||||
|
idwCfg := &idlewatcher.Config{
|
||||||
|
Docker: &idlewatcher.DockerConfig{
|
||||||
|
DockerHost: c.DockerHost,
|
||||||
|
ContainerID: c.ContainerID,
|
||||||
|
ContainerName: c.ContainerName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := utils.Deserialize(cfg, idwCfg)
|
||||||
|
if err != nil {
|
||||||
|
gperr.LogWarn("invalid idlewatcher config", gperr.PrependSubject(c.ContainerName, err))
|
||||||
|
} else {
|
||||||
|
c.IdlewatcherConfig = idwCfg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type containerHelper struct {
|
type containerHelper struct {
|
||||||
*container.Summary
|
*container.SummaryTrimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDeleteLabel gets the value of a label and then deletes it from the container.
|
// getDeleteLabel gets the value of a label and then deletes it from the container.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func TestContainerExplicit(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
c := FromDocker(&container.Summary{Names: []string{"test"}, State: "test", Labels: tt.labels}, "")
|
c := FromDocker(&container.SummaryTrimmed{Names: []string{"test"}, State: "test", Labels: tt.labels}, "")
|
||||||
ExpectEqual(t, c.IsExplicit, tt.isExplicit)
|
ExpectEqual(t, c.IsExplicit, tt.isExplicit)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
package idlewatcher
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
containerMeta struct {
|
|
||||||
ContainerID, ContainerName string
|
|
||||||
}
|
|
||||||
containerState struct {
|
|
||||||
running bool
|
|
||||||
ready bool
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (w *Watcher) ContainerID() string {
|
|
||||||
return w.route.ContainerInfo().ContainerID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) ContainerName() string {
|
|
||||||
return w.route.ContainerInfo().ContainerName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) containerStop(ctx context.Context) error {
|
|
||||||
return w.client.ContainerStop(ctx, w.ContainerID(), container.StopOptions{
|
|
||||||
Signal: string(w.Config().StopSignal),
|
|
||||||
Timeout: &w.Config().StopTimeout,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) containerPause(ctx context.Context) error {
|
|
||||||
return w.client.ContainerPause(ctx, w.ContainerID())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) containerKill(ctx context.Context) error {
|
|
||||||
return w.client.ContainerKill(ctx, w.ContainerID(), string(w.Config().StopSignal))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) containerUnpause(ctx context.Context) error {
|
|
||||||
return w.client.ContainerUnpause(ctx, w.ContainerID())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) containerStart(ctx context.Context) error {
|
|
||||||
return w.client.ContainerStart(ctx, w.ContainerID(), container.StartOptions{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) containerStatus() (string, error) {
|
|
||||||
ctx, cancel := context.WithTimeoutCause(w.task.Context(), dockerReqTimeout, errors.New("docker request timeout"))
|
|
||||||
defer cancel()
|
|
||||||
json, err := w.client.ContainerInspect(ctx, w.ContainerID())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return json.State.Status, nil
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package idlewatcher
|
|
||||||
|
|
||||||
func (w *Watcher) running() bool {
|
|
||||||
return w.state.Load().running
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) ready() bool {
|
|
||||||
return w.state.Load().ready
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) error() error {
|
|
||||||
return w.state.Load().err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) setReady() {
|
|
||||||
w.state.Store(&containerState{
|
|
||||||
running: true,
|
|
||||||
ready: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) setStarting() {
|
|
||||||
w.state.Store(&containerState{
|
|
||||||
running: true,
|
|
||||||
ready: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) setNapping() {
|
|
||||||
w.setError(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) setError(err error) {
|
|
||||||
w.state.Store(&containerState{
|
|
||||||
running: false,
|
|
||||||
ready: false,
|
|
||||||
err: err,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Config struct {
|
|
||||||
IdleTimeout time.Duration `json:"idle_timeout,omitempty"`
|
|
||||||
WakeTimeout time.Duration `json:"wake_timeout,omitempty"`
|
|
||||||
StopTimeout int `json:"stop_timeout,omitempty"` // docker api takes integer seconds for timeout argument
|
|
||||||
StopMethod StopMethod `json:"stop_method,omitempty"`
|
|
||||||
StopSignal Signal `json:"stop_signal,omitempty"`
|
|
||||||
StartEndpoint string `json:"start_endpoint,omitempty"` // Optional path that must be hit to start container
|
|
||||||
}
|
|
||||||
StopMethod string
|
|
||||||
Signal string
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
StopMethodPause StopMethod = "pause"
|
|
||||||
StopMethodStop StopMethod = "stop"
|
|
||||||
StopMethodKill StopMethod = "kill"
|
|
||||||
)
|
|
||||||
|
|
||||||
var validSignals = map[string]struct{}{
|
|
||||||
"": {},
|
|
||||||
"SIGINT": {}, "SIGTERM": {}, "SIGHUP": {}, "SIGQUIT": {},
|
|
||||||
"INT": {}, "TERM": {}, "HUP": {}, "QUIT": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateConfig(cont *docker.Container) (*Config, gperr.Error) {
|
|
||||||
if cont == nil || cont.IdleTimeout == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
errs := gperr.NewBuilder("invalid idlewatcher config")
|
|
||||||
|
|
||||||
idleTimeout := gperr.Collect(errs, validateDurationPostitive, cont.IdleTimeout)
|
|
||||||
wakeTimeout := gperr.Collect(errs, validateDurationPostitive, cont.WakeTimeout)
|
|
||||||
stopTimeout := gperr.Collect(errs, validateDurationPostitive, cont.StopTimeout)
|
|
||||||
stopMethod := gperr.Collect(errs, validateStopMethod, cont.StopMethod)
|
|
||||||
signal := gperr.Collect(errs, validateSignal, cont.StopSignal)
|
|
||||||
startEndpoint := gperr.Collect(errs, validateStartEndpoint, cont.StartEndpoint)
|
|
||||||
|
|
||||||
if errs.HasError() {
|
|
||||||
return nil, errs.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Config{
|
|
||||||
IdleTimeout: idleTimeout,
|
|
||||||
WakeTimeout: wakeTimeout,
|
|
||||||
StopTimeout: int(stopTimeout.Seconds()),
|
|
||||||
StopMethod: stopMethod,
|
|
||||||
StopSignal: signal,
|
|
||||||
StartEndpoint: startEndpoint,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateDurationPostitive(value string) (time.Duration, error) {
|
|
||||||
d, err := time.ParseDuration(value)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if d < 0 {
|
|
||||||
return 0, errors.New("duration must be positive")
|
|
||||||
}
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateSignal(s string) (Signal, error) {
|
|
||||||
if _, ok := validSignals[s]; ok {
|
|
||||||
return Signal(s), nil
|
|
||||||
}
|
|
||||||
return "", errors.New("invalid signal " + s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateStopMethod(s string) (StopMethod, error) {
|
|
||||||
sm := StopMethod(s)
|
|
||||||
switch sm {
|
|
||||||
case StopMethodPause, StopMethodStop, StopMethodKill:
|
|
||||||
return sm, nil
|
|
||||||
default:
|
|
||||||
return "", errors.New("invalid stop method " + s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateStartEndpoint(s string) (string, error) {
|
|
||||||
if s == "" {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
// checks needed as of Go 1.6 because of change https://github.com/golang/go/commit/617c93ce740c3c3cc28cdd1a0d712be183d0b328#diff-6c2d018290e298803c0c9419d8739885L195
|
|
||||||
// emulate browser and strip the '#' suffix prior to validation. see issue-#237
|
|
||||||
if i := strings.Index(s, "#"); i > -1 {
|
|
||||||
s = s[:i]
|
|
||||||
}
|
|
||||||
if len(s) == 0 {
|
|
||||||
return "", errors.New("start endpoint must not be empty if defined")
|
|
||||||
}
|
|
||||||
if _, err := url.ParseRequestURI(s); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
package idlewatcher
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
"github.com/yusing/go-proxy/internal/metrics"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
|
||||||
net "github.com/yusing/go-proxy/internal/net/types"
|
|
||||||
route "github.com/yusing/go-proxy/internal/route/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Waker = types.Waker
|
|
||||||
waker struct {
|
|
||||||
_ U.NoCopy
|
|
||||||
|
|
||||||
rp *reverseproxy.ReverseProxy
|
|
||||||
stream net.Stream
|
|
||||||
hc health.HealthChecker
|
|
||||||
metric *metrics.Gauge
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
idleWakerCheckInterval = 100 * time.Millisecond
|
|
||||||
idleWakerCheckTimeout = time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: support stream
|
|
||||||
|
|
||||||
func newWaker(parent task.Parent, route route.Route, rp *reverseproxy.ReverseProxy, stream net.Stream) (Waker, gperr.Error) {
|
|
||||||
hcCfg := route.HealthCheckConfig()
|
|
||||||
hcCfg.Timeout = idleWakerCheckTimeout
|
|
||||||
|
|
||||||
waker := &waker{
|
|
||||||
rp: rp,
|
|
||||||
stream: stream,
|
|
||||||
}
|
|
||||||
watcher, err := registerWatcher(parent, route, waker)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gperr.Errorf("register watcher: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case route.IsAgent():
|
|
||||||
waker.hc = monitor.NewAgentProxiedMonitor(route.Agent(), hcCfg, monitor.AgentTargetFromURL(route.TargetURL()))
|
|
||||||
case rp != nil:
|
|
||||||
waker.hc = monitor.NewHTTPHealthChecker(route.TargetURL(), hcCfg)
|
|
||||||
case stream != nil:
|
|
||||||
waker.hc = monitor.NewRawHealthChecker(route.TargetURL(), hcCfg)
|
|
||||||
default:
|
|
||||||
panic("both nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
return watcher, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// lifetime should follow route provider.
|
|
||||||
func NewHTTPWaker(parent task.Parent, route route.Route, rp *reverseproxy.ReverseProxy) (Waker, gperr.Error) {
|
|
||||||
return newWaker(parent, route, rp, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStreamWaker(parent task.Parent, route route.Route, stream net.Stream) (Waker, gperr.Error) {
|
|
||||||
return newWaker(parent, route, nil, stream)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start implements health.HealthMonitor.
|
|
||||||
func (w *Watcher) Start(parent task.Parent) gperr.Error {
|
|
||||||
w.task.OnCancel("route_cleanup", func() {
|
|
||||||
parent.Finish(w.task.FinishCause())
|
|
||||||
if w.metric != nil {
|
|
||||||
w.metric.Reset()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task implements health.HealthMonitor.
|
|
||||||
func (w *Watcher) Task() *task.Task {
|
|
||||||
return w.task
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finish implements health.HealthMonitor.
|
|
||||||
func (w *Watcher) Finish(reason any) {
|
|
||||||
if w.stream != nil {
|
|
||||||
w.stream.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name implements health.HealthMonitor.
|
|
||||||
func (w *Watcher) Name() string {
|
|
||||||
return w.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// String implements health.HealthMonitor.
|
|
||||||
func (w *Watcher) String() string {
|
|
||||||
return w.ContainerName()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uptime implements health.HealthMonitor.
|
|
||||||
func (w *Watcher) Uptime() time.Duration {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Latency implements health.HealthMonitor.
|
|
||||||
func (w *Watcher) Latency() time.Duration {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status implements health.HealthMonitor.
|
|
||||||
func (w *Watcher) Status() health.Status {
|
|
||||||
state := w.state.Load()
|
|
||||||
if state.err != nil {
|
|
||||||
return health.StatusError
|
|
||||||
}
|
|
||||||
if state.ready {
|
|
||||||
return health.StatusHealthy
|
|
||||||
}
|
|
||||||
if state.running {
|
|
||||||
return health.StatusStarting
|
|
||||||
}
|
|
||||||
return health.StatusNapping
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) checkUpdateState() (ready bool, err error) {
|
|
||||||
// already ready
|
|
||||||
if w.ready() {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !w.running() {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.metric != nil {
|
|
||||||
defer w.metric.Set(float64(w.Status()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// the new container info not yet updated
|
|
||||||
if w.hc.URL().Host == "" {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := w.hc.CheckHealth()
|
|
||||||
if err != nil {
|
|
||||||
w.setError(err)
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Healthy {
|
|
||||||
w.setReady()
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
w.setStarting()
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON implements health.HealthMonitor.
|
|
||||||
func (w *Watcher) MarshalJSON() ([]byte, error) {
|
|
||||||
var url *net.URL
|
|
||||||
if w.hc.URL().Port() != "0" {
|
|
||||||
url = w.hc.URL()
|
|
||||||
}
|
|
||||||
var detail string
|
|
||||||
if err := w.error(); err != nil {
|
|
||||||
detail = err.Error()
|
|
||||||
}
|
|
||||||
return (&monitor.JSONRepresentation{
|
|
||||||
Name: w.Name(),
|
|
||||||
Status: w.Status(),
|
|
||||||
Config: w.hc.Config(),
|
|
||||||
URL: url,
|
|
||||||
Detail: detail,
|
|
||||||
}).MarshalJSON()
|
|
||||||
}
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
package idlewatcher
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
|
||||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
|
||||||
route "github.com/yusing/go-proxy/internal/route/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/atomic"
|
|
||||||
"github.com/yusing/go-proxy/internal/watcher"
|
|
||||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Watcher struct {
|
|
||||||
_ U.NoCopy
|
|
||||||
|
|
||||||
zerolog.Logger
|
|
||||||
|
|
||||||
*waker
|
|
||||||
|
|
||||||
route route.Route
|
|
||||||
|
|
||||||
client *docker.SharedClient
|
|
||||||
state atomic.Value[*containerState]
|
|
||||||
|
|
||||||
stopByMethod StopCallback // send a docker command w.r.t. `stop_method`
|
|
||||||
ticker *time.Ticker
|
|
||||||
lastReset time.Time
|
|
||||||
task *task.Task
|
|
||||||
}
|
|
||||||
|
|
||||||
StopCallback func() error
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
watcherMap = make(map[string]*Watcher)
|
|
||||||
watcherMapMu sync.RWMutex
|
|
||||||
|
|
||||||
errShouldNotReachHere = errors.New("should not reach here")
|
|
||||||
)
|
|
||||||
|
|
||||||
const dockerReqTimeout = 3 * time.Second
|
|
||||||
|
|
||||||
func registerWatcher(parent task.Parent, route route.Route, waker *waker) (*Watcher, error) {
|
|
||||||
cfg := route.IdlewatcherConfig()
|
|
||||||
cont := route.ContainerInfo()
|
|
||||||
key := cont.ContainerID
|
|
||||||
|
|
||||||
watcherMapMu.Lock()
|
|
||||||
defer watcherMapMu.Unlock()
|
|
||||||
w, ok := watcherMap[key]
|
|
||||||
if !ok {
|
|
||||||
client, err := docker.NewClient(cont.DockerHost)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
w = &Watcher{
|
|
||||||
Logger: logging.With().Str("name", cont.ContainerName).Logger(),
|
|
||||||
client: client,
|
|
||||||
task: parent.Subtask("idlewatcher." + cont.ContainerName),
|
|
||||||
ticker: time.NewTicker(cfg.IdleTimeout),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: possible race condition here
|
|
||||||
w.waker = waker
|
|
||||||
w.route = route
|
|
||||||
w.ticker.Reset(cfg.IdleTimeout)
|
|
||||||
|
|
||||||
if cont.Running {
|
|
||||||
w.setStarting()
|
|
||||||
} else {
|
|
||||||
w.setNapping()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
w.stopByMethod = w.getStopCallback()
|
|
||||||
watcherMap[key] = w
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
cause := w.watchUntilDestroy()
|
|
||||||
|
|
||||||
watcherMapMu.Lock()
|
|
||||||
defer watcherMapMu.Unlock()
|
|
||||||
delete(watcherMap, key)
|
|
||||||
|
|
||||||
w.ticker.Stop()
|
|
||||||
w.client.Close()
|
|
||||||
w.task.Finish(cause)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
return w, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) Config() *idlewatcher.Config {
|
|
||||||
return w.route.IdlewatcherConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) Wake() error {
|
|
||||||
return w.wakeIfStopped()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WakeDebug logs a debug message related to waking the container.
|
|
||||||
func (w *Watcher) WakeDebug() *zerolog.Event {
|
|
||||||
//nolint:zerologlint
|
|
||||||
return w.Debug().Str("action", "wake")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) WakeTrace() *zerolog.Event {
|
|
||||||
//nolint:zerologlint
|
|
||||||
return w.Trace().Str("action", "wake")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) WakeError(err error) {
|
|
||||||
w.Err(err).Str("action", "wake").Msg("error")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) wakeIfStopped() error {
|
|
||||||
if w.running() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
status, err := w.containerStatus()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(w.task.Context(), w.Config().WakeTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// !Hard coded here since theres no constants from Docker API
|
|
||||||
switch status {
|
|
||||||
case "exited", "dead":
|
|
||||||
return w.containerStart(ctx)
|
|
||||||
case "paused":
|
|
||||||
return w.containerUnpause(ctx)
|
|
||||||
case "running":
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return gperr.Errorf("unexpected container status: %s", status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) getStopCallback() StopCallback {
|
|
||||||
var cb func(context.Context) error
|
|
||||||
switch w.Config().StopMethod {
|
|
||||||
case idlewatcher.StopMethodPause:
|
|
||||||
cb = w.containerPause
|
|
||||||
case idlewatcher.StopMethodStop:
|
|
||||||
cb = w.containerStop
|
|
||||||
case idlewatcher.StopMethodKill:
|
|
||||||
cb = w.containerKill
|
|
||||||
default:
|
|
||||||
panic(errShouldNotReachHere)
|
|
||||||
}
|
|
||||||
return func() error {
|
|
||||||
ctx, cancel := context.WithTimeout(w.task.Context(), time.Duration(w.Config().StopTimeout)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
return cb(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) resetIdleTimer() {
|
|
||||||
w.Trace().Msg("reset idle timer")
|
|
||||||
w.ticker.Reset(w.Config().IdleTimeout)
|
|
||||||
w.lastReset = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) expires() time.Time {
|
|
||||||
return w.lastReset.Add(w.Config().IdleTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Watcher) getEventCh(ctx context.Context, dockerWatcher *watcher.DockerWatcher) (eventCh <-chan events.Event, errCh <-chan gperr.Error) {
|
|
||||||
eventCh, errCh = dockerWatcher.EventsWithOptions(ctx, watcher.DockerListOptions{
|
|
||||||
Filters: watcher.NewDockerFilter(
|
|
||||||
watcher.DockerFilterContainer,
|
|
||||||
watcher.DockerFilterContainerNameID(w.route.ContainerInfo().ContainerID),
|
|
||||||
watcher.DockerFilterStart,
|
|
||||||
watcher.DockerFilterStop,
|
|
||||||
watcher.DockerFilterDie,
|
|
||||||
watcher.DockerFilterKill,
|
|
||||||
watcher.DockerFilterDestroy,
|
|
||||||
watcher.DockerFilterPause,
|
|
||||||
watcher.DockerFilterUnpause,
|
|
||||||
),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// watchUntilDestroy waits for the container to be created, started, or unpaused,
|
|
||||||
// and then reset the idle timer.
|
|
||||||
//
|
|
||||||
// When the container is stopped, paused,
|
|
||||||
// or killed, the idle timer is stopped and the ContainerRunning flag is set to false.
|
|
||||||
//
|
|
||||||
// When the idle timer fires, the container is stopped according to the
|
|
||||||
// stop method.
|
|
||||||
//
|
|
||||||
// it exits only if the context is canceled, the container is destroyed,
|
|
||||||
// errors occurred on docker client, or route provider died (mainly caused by config reload).
|
|
||||||
func (w *Watcher) watchUntilDestroy() (returnCause error) {
|
|
||||||
eventCtx, eventCancel := context.WithCancel(w.task.Context())
|
|
||||||
defer eventCancel()
|
|
||||||
|
|
||||||
dockerWatcher := watcher.NewDockerWatcher(w.client.DaemonHost())
|
|
||||||
dockerEventCh, dockerEventErrCh := w.getEventCh(eventCtx, dockerWatcher)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-w.task.Context().Done():
|
|
||||||
return w.task.FinishCause()
|
|
||||||
case err := <-dockerEventErrCh:
|
|
||||||
if !err.Is(context.Canceled) {
|
|
||||||
gperr.LogError("idlewatcher error", err, &w.Logger)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
case e := <-dockerEventCh:
|
|
||||||
switch {
|
|
||||||
case e.Action == events.ActionContainerDestroy:
|
|
||||||
w.setError(errors.New("container destroyed"))
|
|
||||||
w.Info().Str("reason", "container destroyed").Msg("watcher stopped")
|
|
||||||
return errors.New("container destroyed")
|
|
||||||
// create / start / unpause
|
|
||||||
case e.Action.IsContainerWake():
|
|
||||||
w.setStarting()
|
|
||||||
w.resetIdleTimer()
|
|
||||||
w.Info().Msg("awaken")
|
|
||||||
case e.Action.IsContainerSleep(): // stop / pause / kil
|
|
||||||
w.setNapping()
|
|
||||||
w.resetIdleTimer()
|
|
||||||
w.ticker.Stop()
|
|
||||||
default:
|
|
||||||
w.Error().Msg("unexpected docker event: " + e.String())
|
|
||||||
}
|
|
||||||
// container name changed should also change the container id
|
|
||||||
// if w.ContainerName != e.ActorName {
|
|
||||||
// w.Debug().Msgf("renamed %s -> %s", w.ContainerName, e.ActorName)
|
|
||||||
// w.ContainerName = e.ActorName
|
|
||||||
// }
|
|
||||||
// if w.ContainerID != e.ActorID {
|
|
||||||
// w.Debug().Msgf("id changed %s -> %s", w.ContainerID, e.ActorID)
|
|
||||||
// w.ContainerID = e.ActorID
|
|
||||||
// // recreate event stream
|
|
||||||
// eventCancel()
|
|
||||||
|
|
||||||
// eventCtx, eventCancel = context.WithCancel(w.task.Context())
|
|
||||||
// defer eventCancel()
|
|
||||||
// dockerEventCh, dockerEventErrCh = w.getEventCh(eventCtx, dockerWatcher)
|
|
||||||
// }
|
|
||||||
case <-w.ticker.C:
|
|
||||||
w.ticker.Stop()
|
|
||||||
if w.running() {
|
|
||||||
err := w.stopByMethod()
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, context.Canceled):
|
|
||||||
continue
|
|
||||||
case err != nil:
|
|
||||||
if errors.Is(err, context.DeadlineExceeded) {
|
|
||||||
err = errors.New("timeout waiting for container to stop, please set a higher value for `stop_timeout`")
|
|
||||||
}
|
|
||||||
w.Err(err).Msgf("container stop with method %q failed", w.Config().StopMethod)
|
|
||||||
default:
|
|
||||||
w.Info().Str("reason", "idle timeout").Msg("container stopped")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package docker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Inspect(dockerHost string, containerID string) (*Container, error) {
|
|
||||||
client, err := NewClient(dockerHost)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer client.Close()
|
|
||||||
return client.Inspect(containerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SharedClient) Inspect(containerID string) (*Container, error) {
|
|
||||||
ctx, cancel := context.WithTimeoutCause(context.Background(), 3*time.Second, errors.New("docker container inspect timeout"))
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
json, err := c.ContainerInspect(ctx, containerID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return FromInspectResponse(json, c.key), nil
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,7 @@ var listOptions = container.ListOptions{
|
|||||||
All: true,
|
All: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListContainers(clientHost string) ([]container.Summary, error) {
|
func ListContainers(clientHost string) ([]container.SummaryTrimmed, error) {
|
||||||
dockerClient, err := NewClient(clientHost)
|
dockerClient, err := NewClient(clientHost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging/accesslog"
|
||||||
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
|
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/accesslog"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/middleware/errorpage"
|
"github.com/yusing/go-proxy/internal/net/gphttp/middleware/errorpage"
|
||||||
"github.com/yusing/go-proxy/internal/route/routes"
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
route "github.com/yusing/go-proxy/internal/route/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
@@ -20,7 +19,7 @@ import (
|
|||||||
type Entrypoint struct {
|
type Entrypoint struct {
|
||||||
middleware *middleware.Middleware
|
middleware *middleware.Middleware
|
||||||
accessLogger *accesslog.AccessLogger
|
accessLogger *accesslog.AccessLogger
|
||||||
findRouteFunc func(host string) (route.HTTPRoute, error)
|
findRouteFunc func(host string) (routes.HTTPRoute, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrNoSuchRoute = errors.New("no such route")
|
var ErrNoSuchRoute = errors.New("no such route")
|
||||||
@@ -55,13 +54,13 @@ func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.Config) (err error) {
|
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) (err error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
ep.accessLogger = nil
|
ep.accessLogger = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ep.accessLogger, err = accesslog.NewFileAccessLogger(parent, cfg)
|
ep.accessLogger, err = accesslog.NewAccessLogger(parent, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -108,7 +107,7 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func findRouteAnyDomain(host string) (route.HTTPRoute, error) {
|
func findRouteAnyDomain(host string) (routes.HTTPRoute, error) {
|
||||||
hostSplit := strutils.SplitRune(host, '.')
|
hostSplit := strutils.SplitRune(host, '.')
|
||||||
target := hostSplit[0]
|
target := hostSplit[0]
|
||||||
|
|
||||||
@@ -118,19 +117,19 @@ func findRouteAnyDomain(host string) (route.HTTPRoute, error) {
|
|||||||
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, target)
|
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findRouteByDomains(domains []string) func(host string) (route.HTTPRoute, error) {
|
func findRouteByDomains(domains []string) func(host string) (routes.HTTPRoute, error) {
|
||||||
return func(host string) (route.HTTPRoute, error) {
|
return func(host string) (routes.HTTPRoute, error) {
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
if strings.HasSuffix(host, domain) {
|
if strings.HasSuffix(host, domain) {
|
||||||
target := strings.TrimSuffix(host, domain)
|
target := strings.TrimSuffix(host, domain)
|
||||||
if r, ok := routes.GetHTTPRoute(target); ok {
|
if r, ok := routes.HTTP.Get(target); ok {
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback to exact match
|
// fallback to exact match
|
||||||
if r, ok := routes.GetHTTPRoute(host); ok {
|
if r, ok := routes.HTTP.Get(host); ok {
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, host)
|
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, host)
|
||||||
|
|||||||
@@ -5,37 +5,43 @@ import (
|
|||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/route"
|
"github.com/yusing/go-proxy/internal/route"
|
||||||
"github.com/yusing/go-proxy/internal/route/routes"
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
|
||||||
|
expect "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var ep = NewEntrypoint()
|
||||||
r route.ReveseProxyRoute
|
|
||||||
ep = NewEntrypoint()
|
func addRoute(alias string) {
|
||||||
)
|
routes.HTTP.Add(&route.ReveseProxyRoute{
|
||||||
|
Route: &route.Route{
|
||||||
|
Alias: alias,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func run(t *testing.T, match []string, noMatch []string) {
|
func run(t *testing.T, match []string, noMatch []string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Cleanup(routes.TestClear)
|
t.Cleanup(routes.Clear)
|
||||||
t.Cleanup(func() { ep.SetFindRouteDomains(nil) })
|
t.Cleanup(func() { ep.SetFindRouteDomains(nil) })
|
||||||
|
|
||||||
for _, test := range match {
|
for _, test := range match {
|
||||||
t.Run(test, func(t *testing.T) {
|
t.Run(test, func(t *testing.T) {
|
||||||
found, err := ep.findRouteFunc(test)
|
found, err := ep.findRouteFunc(test)
|
||||||
ExpectNoError(t, err)
|
expect.NoError(t, err)
|
||||||
ExpectTrue(t, found == &r)
|
expect.NotNil(t, found)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range noMatch {
|
for _, test := range noMatch {
|
||||||
t.Run(test, func(t *testing.T) {
|
t.Run(test, func(t *testing.T) {
|
||||||
_, err := ep.findRouteFunc(test)
|
_, err := ep.findRouteFunc(test)
|
||||||
ExpectError(t, ErrNoSuchRoute, err)
|
expect.ErrorIs(t, ErrNoSuchRoute, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindRouteAnyDomain(t *testing.T) {
|
func TestFindRouteAnyDomain(t *testing.T) {
|
||||||
routes.SetHTTPRoute("app1", &r)
|
addRoute("app1")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1.com",
|
"app1.com",
|
||||||
@@ -66,7 +72,7 @@ func TestFindRouteExactHostMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
routes.SetHTTPRoute(test, &r)
|
addRoute(test)
|
||||||
}
|
}
|
||||||
|
|
||||||
run(t, tests, testsNoMatch)
|
run(t, tests, testsNoMatch)
|
||||||
@@ -78,7 +84,7 @@ func TestFindRouteByDomains(t *testing.T) {
|
|||||||
".sub.domain.com",
|
".sub.domain.com",
|
||||||
})
|
})
|
||||||
|
|
||||||
routes.SetHTTPRoute("app1", &r)
|
addRoute("app1")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1.domain.com",
|
"app1.domain.com",
|
||||||
@@ -103,7 +109,7 @@ func TestFindRouteByDomainsExactMatch(t *testing.T) {
|
|||||||
".sub.domain.com",
|
".sub.domain.com",
|
||||||
})
|
})
|
||||||
|
|
||||||
routes.SetHTTPRoute("app1.foo.bar", &r)
|
addRoute("app1.foo.bar")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1.foo.bar", // exact match
|
"app1.foo.bar", // exact match
|
||||||
|
|||||||
@@ -5,44 +5,69 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type noLock struct{}
|
||||||
|
|
||||||
|
func (noLock) Lock() {}
|
||||||
|
func (noLock) Unlock() {}
|
||||||
|
func (noLock) RLock() {}
|
||||||
|
func (noLock) RUnlock() {}
|
||||||
|
|
||||||
|
type rwLock interface {
|
||||||
|
sync.Locker
|
||||||
|
RLock()
|
||||||
|
RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
type Builder struct {
|
type Builder struct {
|
||||||
about string
|
about string
|
||||||
errs []error
|
errs []error
|
||||||
sync.Mutex
|
rwLock
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBuilder(about string) *Builder {
|
type multiline struct {
|
||||||
return &Builder{about: about}
|
*Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuilder creates a new Builder.
|
||||||
|
//
|
||||||
|
// If about is not provided, the Builder will not have a subject
|
||||||
|
// and will expand when adding to another builder.
|
||||||
|
func NewBuilder(about ...string) *Builder {
|
||||||
|
if len(about) == 0 {
|
||||||
|
return &Builder{rwLock: noLock{}}
|
||||||
|
}
|
||||||
|
return &Builder{about: about[0], rwLock: noLock{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBuilderWithConcurrency(about ...string) *Builder {
|
||||||
|
if len(about) == 0 {
|
||||||
|
return &Builder{rwLock: new(sync.RWMutex)}
|
||||||
|
}
|
||||||
|
return &Builder{about: about[0], rwLock: new(sync.RWMutex)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) EnableConcurrency() {
|
||||||
|
b.rwLock = new(sync.RWMutex)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) About() string {
|
func (b *Builder) About() string {
|
||||||
if !b.HasError() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return b.about
|
return b.about
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
|
||||||
func (b *Builder) HasError() bool {
|
func (b *Builder) HasError() bool {
|
||||||
|
// no need to lock, when this is called, the Builder is not used anymore
|
||||||
return len(b.errs) > 0
|
return len(b.errs) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) error() Error {
|
func (b *Builder) Error() Error {
|
||||||
if !b.HasError() {
|
if len(b.errs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &nestedError{Err: New(b.about), Extras: b.errs}
|
return &nestedError{Err: New(b.about), Extras: b.errs}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) Error() Error {
|
|
||||||
if len(b.errs) == 1 {
|
|
||||||
return wrap(b.errs[0])
|
|
||||||
}
|
|
||||||
return b.error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Builder) String() string {
|
func (b *Builder) String() string {
|
||||||
err := b.error()
|
err := b.Error()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -52,15 +77,19 @@ func (b *Builder) String() string {
|
|||||||
// Add adds an error to the Builder.
|
// Add adds an error to the Builder.
|
||||||
//
|
//
|
||||||
// adding nil is no-op.
|
// adding nil is no-op.
|
||||||
func (b *Builder) Add(err error) *Builder {
|
func (b *Builder) Add(err error) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return b
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Lock()
|
b.Lock()
|
||||||
defer b.Unlock()
|
defer b.Unlock()
|
||||||
|
|
||||||
switch err := wrap(err).(type) {
|
b.add(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) add(err error) {
|
||||||
|
switch err := err.(type) {
|
||||||
case *baseError:
|
case *baseError:
|
||||||
b.errs = append(b.errs, err.Err)
|
b.errs = append(b.errs, err.Err)
|
||||||
case *nestedError:
|
case *nestedError:
|
||||||
@@ -69,21 +98,20 @@ func (b *Builder) Add(err error) *Builder {
|
|||||||
} else {
|
} else {
|
||||||
b.errs = append(b.errs, err)
|
b.errs = append(b.errs, err)
|
||||||
}
|
}
|
||||||
|
case *MultilineError:
|
||||||
|
b.add(&err.nestedError)
|
||||||
default:
|
default:
|
||||||
panic("bug: should not reach here")
|
b.errs = append(b.errs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) Adds(err string) *Builder {
|
func (b *Builder) Adds(err string) {
|
||||||
b.Lock()
|
b.Lock()
|
||||||
defer b.Unlock()
|
defer b.Unlock()
|
||||||
b.errs = append(b.errs, newError(err))
|
b.errs = append(b.errs, newError(err))
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) Addf(format string, args ...any) *Builder {
|
func (b *Builder) Addf(format string, args ...any) {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
b.Lock()
|
b.Lock()
|
||||||
defer b.Unlock()
|
defer b.Unlock()
|
||||||
@@ -91,13 +119,11 @@ func (b *Builder) Addf(format string, args ...any) *Builder {
|
|||||||
} else {
|
} else {
|
||||||
b.Adds(format)
|
b.Adds(format)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) AddFrom(other *Builder, flatten bool) *Builder {
|
func (b *Builder) AddFrom(other *Builder, flatten bool) {
|
||||||
if other == nil || !other.HasError() {
|
if other == nil || !other.HasError() {
|
||||||
return b
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Lock()
|
b.Lock()
|
||||||
@@ -105,26 +131,32 @@ func (b *Builder) AddFrom(other *Builder, flatten bool) *Builder {
|
|||||||
if flatten {
|
if flatten {
|
||||||
b.errs = append(b.errs, other.errs...)
|
b.errs = append(b.errs, other.errs...)
|
||||||
} else {
|
} else {
|
||||||
b.errs = append(b.errs, other.error())
|
b.errs = append(b.errs, other.Error())
|
||||||
}
|
}
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) AddRange(errs ...error) *Builder {
|
func (b *Builder) AddRange(errs ...error) {
|
||||||
b.Lock()
|
nonNilErrs := make([]error, 0, len(errs))
|
||||||
defer b.Unlock()
|
|
||||||
|
|
||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.errs = append(b.errs, err)
|
nonNilErrs = append(nonNilErrs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b
|
b.Lock()
|
||||||
|
defer b.Unlock()
|
||||||
|
|
||||||
|
for _, err := range nonNilErrs {
|
||||||
|
b.add(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) ForEach(fn func(error)) {
|
func (b *Builder) ForEach(fn func(error)) {
|
||||||
for _, err := range b.errs {
|
b.RLock()
|
||||||
|
errs := b.errs
|
||||||
|
b.RUnlock()
|
||||||
|
|
||||||
|
for _, err := range errs {
|
||||||
fn(err)
|
fn(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||||
|
expect "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBaseString(t *testing.T) {
|
func TestBaseString(t *testing.T) {
|
||||||
ExpectEqual(t, New("error").Error(), "error")
|
expect.Equal(t, New("error").Error(), "error")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseWithSubject(t *testing.T) {
|
func TestBaseWithSubject(t *testing.T) {
|
||||||
@@ -17,13 +18,13 @@ func TestBaseWithSubject(t *testing.T) {
|
|||||||
withSubject := err.Subject("foo")
|
withSubject := err.Subject("foo")
|
||||||
withSubjectf := err.Subjectf("%s %s", "foo", "bar")
|
withSubjectf := err.Subjectf("%s %s", "foo", "bar")
|
||||||
|
|
||||||
ExpectError(t, err, withSubject)
|
expect.ErrorIs(t, err, withSubject)
|
||||||
ExpectEqual(t, withSubject.Error(), "foo: error")
|
expect.Equal(t, ansi.StripANSI(withSubject.Error()), "foo: error")
|
||||||
ExpectTrue(t, withSubject.Is(err))
|
expect.True(t, withSubject.Is(err))
|
||||||
|
|
||||||
ExpectError(t, err, withSubjectf)
|
expect.ErrorIs(t, err, withSubjectf)
|
||||||
ExpectEqual(t, withSubjectf.Error(), "foo bar: error")
|
expect.Equal(t, ansi.StripANSI(withSubjectf.Error()), "foo bar: error")
|
||||||
ExpectTrue(t, withSubjectf.Is(err))
|
expect.True(t, withSubjectf.Is(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseWithExtra(t *testing.T) {
|
func TestBaseWithExtra(t *testing.T) {
|
||||||
@@ -31,22 +32,22 @@ func TestBaseWithExtra(t *testing.T) {
|
|||||||
extra := New("bar").Subject("baz")
|
extra := New("bar").Subject("baz")
|
||||||
withExtra := err.With(extra)
|
withExtra := err.With(extra)
|
||||||
|
|
||||||
ExpectTrue(t, withExtra.Is(extra))
|
expect.True(t, withExtra.Is(extra))
|
||||||
ExpectTrue(t, withExtra.Is(err))
|
expect.True(t, withExtra.Is(err))
|
||||||
|
|
||||||
ExpectTrue(t, errors.Is(withExtra, extra))
|
expect.True(t, errors.Is(withExtra, extra))
|
||||||
ExpectTrue(t, errors.Is(withExtra, err))
|
expect.True(t, errors.Is(withExtra, err))
|
||||||
|
|
||||||
ExpectTrue(t, strings.Contains(withExtra.Error(), err.Error()))
|
expect.True(t, strings.Contains(withExtra.Error(), err.Error()))
|
||||||
ExpectTrue(t, strings.Contains(withExtra.Error(), extra.Error()))
|
expect.True(t, strings.Contains(withExtra.Error(), extra.Error()))
|
||||||
ExpectTrue(t, strings.Contains(withExtra.Error(), "baz"))
|
expect.True(t, strings.Contains(withExtra.Error(), "baz"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseUnwrap(t *testing.T) {
|
func TestBaseUnwrap(t *testing.T) {
|
||||||
err := errors.New("err")
|
err := errors.New("err")
|
||||||
wrapped := Wrap(err)
|
wrapped := Wrap(err)
|
||||||
|
|
||||||
ExpectError(t, err, errors.Unwrap(wrapped))
|
expect.ErrorIs(t, err, errors.Unwrap(wrapped))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNestedUnwrap(t *testing.T) {
|
func TestNestedUnwrap(t *testing.T) {
|
||||||
@@ -55,24 +56,24 @@ func TestNestedUnwrap(t *testing.T) {
|
|||||||
wrapped := Wrap(err).Subject("foo").With(err2.Subject("bar"))
|
wrapped := Wrap(err).Subject("foo").With(err2.Subject("bar"))
|
||||||
|
|
||||||
unwrapper, ok := wrapped.(interface{ Unwrap() []error })
|
unwrapper, ok := wrapped.(interface{ Unwrap() []error })
|
||||||
ExpectTrue(t, ok)
|
expect.True(t, ok)
|
||||||
|
|
||||||
ExpectError(t, err, wrapped)
|
expect.ErrorIs(t, err, wrapped)
|
||||||
ExpectError(t, err2, wrapped)
|
expect.ErrorIs(t, err2, wrapped)
|
||||||
ExpectEqual(t, len(unwrapper.Unwrap()), 2)
|
expect.Equal(t, len(unwrapper.Unwrap()), 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorIs(t *testing.T) {
|
func TestErrorIs(t *testing.T) {
|
||||||
from := errors.New("error")
|
from := errors.New("error")
|
||||||
err := Wrap(from)
|
err := Wrap(from)
|
||||||
ExpectError(t, from, err)
|
expect.ErrorIs(t, from, err)
|
||||||
|
|
||||||
ExpectTrue(t, err.Is(from))
|
expect.True(t, err.Is(from))
|
||||||
ExpectFalse(t, err.Is(New("error")))
|
expect.False(t, err.Is(New("error")))
|
||||||
|
|
||||||
ExpectTrue(t, errors.Is(err.Subject("foo"), from))
|
expect.True(t, errors.Is(err.Subject("foo"), from))
|
||||||
ExpectTrue(t, errors.Is(err.Withf("foo"), from))
|
expect.True(t, errors.Is(err.Withf("foo"), from))
|
||||||
ExpectTrue(t, errors.Is(err.Subject("foo").Withf("bar"), from))
|
expect.True(t, errors.Is(err.Subject("foo").Withf("bar"), from))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorImmutability(t *testing.T) {
|
func TestErrorImmutability(t *testing.T) {
|
||||||
@@ -82,14 +83,14 @@ func TestErrorImmutability(t *testing.T) {
|
|||||||
for range 3 {
|
for range 3 {
|
||||||
// t.Logf("%d: %v %T %s", i, errors.Unwrap(err), err, err)
|
// t.Logf("%d: %v %T %s", i, errors.Unwrap(err), err, err)
|
||||||
_ = err.Subject("foo")
|
_ = err.Subject("foo")
|
||||||
ExpectFalse(t, strings.Contains(err.Error(), "foo"))
|
expect.False(t, strings.Contains(err.Error(), "foo"))
|
||||||
|
|
||||||
_ = err.With(err2)
|
_ = err.With(err2)
|
||||||
ExpectFalse(t, strings.Contains(err.Error(), "extra"))
|
expect.False(t, strings.Contains(err.Error(), "extra"))
|
||||||
ExpectFalse(t, err.Is(err2))
|
expect.False(t, err.Is(err2))
|
||||||
|
|
||||||
err = err.Subject("bar").Withf("baz")
|
err = err.Subject("bar").Withf("baz")
|
||||||
ExpectTrue(t, err != nil)
|
expect.True(t, err != nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,24 +100,24 @@ func TestErrorWith(t *testing.T) {
|
|||||||
|
|
||||||
err3 := err1.With(err2)
|
err3 := err1.With(err2)
|
||||||
|
|
||||||
ExpectTrue(t, err3.Is(err1))
|
expect.True(t, err3.Is(err1))
|
||||||
ExpectTrue(t, err3.Is(err2))
|
expect.True(t, err3.Is(err2))
|
||||||
|
|
||||||
_ = err2.Subject("foo")
|
_ = err2.Subject("foo")
|
||||||
|
|
||||||
ExpectTrue(t, err3.Is(err1))
|
expect.True(t, err3.Is(err1))
|
||||||
ExpectTrue(t, err3.Is(err2))
|
expect.True(t, err3.Is(err2))
|
||||||
|
|
||||||
// check if err3 is affected by err2.Subject
|
// check if err3 is affected by err2.Subject
|
||||||
ExpectFalse(t, strings.Contains(err3.Error(), "foo"))
|
expect.False(t, strings.Contains(err3.Error(), "foo"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorStringSimple(t *testing.T) {
|
func TestErrorStringSimple(t *testing.T) {
|
||||||
errFailure := New("generic failure")
|
errFailure := New("generic failure")
|
||||||
ne := errFailure.Subject("foo bar")
|
ne := errFailure.Subject("foo bar")
|
||||||
ExpectEqual(t, ne.Error(), "foo bar: generic failure")
|
expect.Equal(t, ansi.StripANSI(ne.Error()), "foo bar: generic failure")
|
||||||
ne = ne.Subject("baz")
|
ne = ne.Subject("baz")
|
||||||
ExpectEqual(t, ne.Error(), "baz > foo bar: generic failure")
|
expect.Equal(t, ansi.StripANSI(ne.Error()), "baz > foo bar: generic failure")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorStringNested(t *testing.T) {
|
func TestErrorStringNested(t *testing.T) {
|
||||||
@@ -153,5 +154,5 @@ func TestErrorStringNested(t *testing.T) {
|
|||||||
• action 3 > inner3: generic failure
|
• action 3 > inner3: generic failure
|
||||||
• 3
|
• 3
|
||||||
• 3`
|
• 3`
|
||||||
ExpectEqual(t, ne.Error(), want)
|
expect.Equal(t, ansi.StripANSI(ne.Error()), want)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package gperr
|
package gperr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
@@ -13,7 +15,13 @@ func log(msg string, err error, level zerolog.Level, logger ...*zerolog.Logger)
|
|||||||
} else {
|
} else {
|
||||||
l = logging.GetLogger()
|
l = logging.GetLogger()
|
||||||
}
|
}
|
||||||
l.WithLevel(level).Msg(msg + ": " + err.Error())
|
l.WithLevel(level).Msg(New(highlight(msg)).With(err).Error())
|
||||||
|
switch level {
|
||||||
|
case zerolog.FatalLevel:
|
||||||
|
os.Exit(1)
|
||||||
|
case zerolog.PanicLevel:
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogFatal(msg string, err error, logger ...*zerolog.Logger) {
|
func LogFatal(msg string, err error, logger ...*zerolog.Logger) {
|
||||||
|
|||||||
45
internal/gperr/multiline.go
Normal file
45
internal/gperr/multiline.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package gperr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MultilineError struct {
|
||||||
|
nestedError
|
||||||
|
}
|
||||||
|
|
||||||
|
func Multiline() *MultilineError {
|
||||||
|
return &MultilineError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MultilineError) add(err error) {
|
||||||
|
m.Extras = append(m.Extras, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MultilineError) Addf(format string, args ...any) *MultilineError {
|
||||||
|
m.add(fmt.Errorf(format, args...))
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MultilineError) Adds(s string) *MultilineError {
|
||||||
|
m.add(newError(s))
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MultilineError) AddLines(lines ...any) *MultilineError {
|
||||||
|
v := reflect.ValueOf(lines)
|
||||||
|
if v.Kind() == reflect.Slice {
|
||||||
|
for i := range v.Len() {
|
||||||
|
switch v := v.Index(i).Interface().(type) {
|
||||||
|
case string:
|
||||||
|
m.add(newError(v))
|
||||||
|
case error:
|
||||||
|
m.add(v)
|
||||||
|
default:
|
||||||
|
m.add(fmt.Errorf("%v", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
29
internal/gperr/multiline_test.go
Normal file
29
internal/gperr/multiline_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package gperr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
expect "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWrapMultiline(t *testing.T) {
|
||||||
|
multiline := Multiline()
|
||||||
|
var wrapper error = wrap(multiline)
|
||||||
|
_, ok := wrapper.(*MultilineError)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("wrapper is not a MultilineError")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrependSubjectMultiline(t *testing.T) {
|
||||||
|
multiline := Multiline()
|
||||||
|
multiline.Addf("line 1 %s", "test")
|
||||||
|
multiline.Adds("line 2")
|
||||||
|
multiline.AddLines([]any{1, "2", 3.0, net.IPv4(127, 0, 0, 1)})
|
||||||
|
multiline.Subject("subject")
|
||||||
|
|
||||||
|
builder := NewBuilder()
|
||||||
|
builder.Add(multiline)
|
||||||
|
expect.Equal(t, len(multiline.Extras), len(builder.errs))
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ type nestedError struct {
|
|||||||
|
|
||||||
func (err nestedError) Subject(subject string) Error {
|
func (err nestedError) Subject(subject string) Error {
|
||||||
if err.Err == nil {
|
if err.Err == nil {
|
||||||
err.Err = newError(subject)
|
err.Err = PrependSubject(subject, errStr(""))
|
||||||
} else {
|
} else {
|
||||||
err.Err = PrependSubject(subject, err.Err)
|
err.Err = PrependSubject(subject, err.Err)
|
||||||
}
|
}
|
||||||
@@ -72,14 +72,13 @@ func (err *nestedError) Error() string {
|
|||||||
return makeLine("<nil>", 0)
|
return makeLine("<nil>", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := make([]string, 0, 1+len(err.Extras))
|
|
||||||
if err.Err != nil {
|
if err.Err != nil {
|
||||||
|
lines := make([]string, 0, 1+len(err.Extras))
|
||||||
lines = append(lines, makeLine(err.Err.Error(), 0))
|
lines = append(lines, makeLine(err.Err.Error(), 0))
|
||||||
lines = append(lines, makeLines(err.Extras, 1)...)
|
lines = append(lines, makeLines(err.Extras, 1)...)
|
||||||
} else {
|
return strutils.JoinLines(lines)
|
||||||
lines = append(lines, makeLines(err.Extras, 0)...)
|
|
||||||
}
|
}
|
||||||
return strutils.JoinLines(lines)
|
return strutils.JoinLines(makeLines(err.Extras, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
@@ -103,8 +102,10 @@ func makeLines(errs []error, level int) []string {
|
|||||||
case *nestedError:
|
case *nestedError:
|
||||||
if err.Err != nil {
|
if err.Err != nil {
|
||||||
lines = append(lines, makeLine(err.Err.Error(), level))
|
lines = append(lines, makeLine(err.Err.Error(), level))
|
||||||
|
lines = append(lines, makeLines(err.Extras, level+1)...)
|
||||||
|
} else {
|
||||||
|
lines = append(lines, makeLines(err.Extras, level)...)
|
||||||
}
|
}
|
||||||
lines = append(lines, makeLines(err.Extras, level+1)...)
|
|
||||||
default:
|
default:
|
||||||
lines = append(lines, makeLine(err.Error(), level))
|
lines = append(lines, makeLine(err.Error(), level))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package gperr
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||||
@@ -59,7 +61,7 @@ func (err *withSubject) Prepend(subject string) *withSubject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (err *withSubject) Is(other error) bool {
|
func (err *withSubject) Is(other error) bool {
|
||||||
return err.Err == other
|
return errors.Is(other, err.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err *withSubject) Unwrap() error {
|
func (err *withSubject) Unwrap() error {
|
||||||
@@ -89,10 +91,8 @@ func (err *withSubject) Error() string {
|
|||||||
|
|
||||||
// MarshalJSON implements the json.Marshaler interface.
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
func (err *withSubject) MarshalJSON() ([]byte, error) {
|
func (err *withSubject) MarshalJSON() ([]byte, error) {
|
||||||
subjects := make([]string, len(err.Subjects))
|
subjects := slices.Clone(err.Subjects)
|
||||||
for i, s := range err.Subjects {
|
slices.Reverse(subjects)
|
||||||
subjects[len(err.Subjects)-i-1] = s
|
|
||||||
}
|
|
||||||
reversed := struct {
|
reversed := struct {
|
||||||
Subjects []string `json:"subjects"`
|
Subjects []string `json:"subjects"`
|
||||||
Err error `json:"err"`
|
Err error `json:"err"`
|
||||||
|
|||||||
@@ -41,6 +41,18 @@ func Wrap(err error, message ...string) Error {
|
|||||||
return &baseError{fmt.Errorf("%s: %w", message[0], err)}
|
return &baseError{fmt.Errorf("%s: %w", message[0], err)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Unwrap(err error) Error {
|
||||||
|
//nolint:errorlint
|
||||||
|
switch err := err.(type) {
|
||||||
|
case interface{ Unwrap() []error }:
|
||||||
|
return &nestedError{Extras: err.Unwrap()}
|
||||||
|
case interface{ Unwrap() error }:
|
||||||
|
return &baseError{err.Unwrap()}
|
||||||
|
default:
|
||||||
|
return &baseError{err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func wrap(err error) Error {
|
func wrap(err error) Error {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package favicon
|
package homepage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
222
internal/homepage/favicon.go
Normal file
222
internal/homepage/favicon.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package homepage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/vincent-petithory/dataurl"
|
||||||
|
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FetchResult struct {
|
||||||
|
Icon []byte
|
||||||
|
StatusCode int
|
||||||
|
ErrMsg string
|
||||||
|
|
||||||
|
contentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
const faviconFetchTimeout = 3 * time.Second
|
||||||
|
|
||||||
|
func (res *FetchResult) OK() bool {
|
||||||
|
return len(res.Icon) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *FetchResult) ContentType() string {
|
||||||
|
if res.contentType == "" {
|
||||||
|
if bytes.HasPrefix(res.Icon, []byte("<svg")) || bytes.HasPrefix(res.Icon, []byte("<?xml")) {
|
||||||
|
return "image/svg+xml"
|
||||||
|
}
|
||||||
|
return "image/x-icon"
|
||||||
|
}
|
||||||
|
return res.contentType
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxRedirectDepth = 5
|
||||||
|
|
||||||
|
func FetchFavIconFromURL(ctx context.Context, iconURL *IconURL) *FetchResult {
|
||||||
|
switch iconURL.IconSource {
|
||||||
|
case IconSourceAbsolute:
|
||||||
|
return fetchIconAbsolute(ctx, iconURL.URL())
|
||||||
|
case IconSourceRelative:
|
||||||
|
return &FetchResult{StatusCode: http.StatusBadRequest, ErrMsg: "unexpected relative icon"}
|
||||||
|
case IconSourceWalkXCode, IconSourceSelfhSt:
|
||||||
|
return fetchKnownIcon(ctx, iconURL)
|
||||||
|
}
|
||||||
|
return &FetchResult{StatusCode: http.StatusBadRequest, ErrMsg: "invalid icon source"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchIconAbsolute(ctx context.Context, url string) *FetchResult {
|
||||||
|
if result := loadIconCache(url); result != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
||||||
|
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "request timeout"}
|
||||||
|
}
|
||||||
|
return &FetchResult{StatusCode: http.StatusInternalServerError, ErrMsg: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := gphttp.Do(req)
|
||||||
|
if err == nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
if err != nil || resp.StatusCode != http.StatusOK {
|
||||||
|
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "connection error"}
|
||||||
|
}
|
||||||
|
|
||||||
|
icon, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return &FetchResult{StatusCode: http.StatusInternalServerError, ErrMsg: "internal error"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(icon) == 0 {
|
||||||
|
return &FetchResult{StatusCode: http.StatusNotFound, ErrMsg: "empty icon"}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &FetchResult{Icon: icon}
|
||||||
|
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
|
||||||
|
res.contentType = contentType
|
||||||
|
}
|
||||||
|
// else leave it empty
|
||||||
|
storeIconCache(url, res)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameSanitizer = strings.NewReplacer(
|
||||||
|
"_", "-",
|
||||||
|
" ", "-",
|
||||||
|
"(", "",
|
||||||
|
")", "",
|
||||||
|
)
|
||||||
|
|
||||||
|
func sanitizeName(name string) string {
|
||||||
|
return strings.ToLower(nameSanitizer.Replace(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchKnownIcon(ctx context.Context, url *IconURL) *FetchResult {
|
||||||
|
// if icon isn't in the list, no need to fetch
|
||||||
|
if !url.HasIcon() {
|
||||||
|
return &FetchResult{StatusCode: http.StatusNotFound, ErrMsg: "no such icon"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchIconAbsolute(ctx, url.URL())
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchIcon(ctx context.Context, filetype, filename string) *FetchResult {
|
||||||
|
result := fetchKnownIcon(ctx, NewSelfhStIconURL(filename, filetype))
|
||||||
|
if result.OK() {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return fetchKnownIcon(ctx, NewWalkXCodeIconURL(filename, filetype))
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindIcon(ctx context.Context, r route, uri string) *FetchResult {
|
||||||
|
if result := loadIconCache(r.Key()); result != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result := fetchIcon(ctx, "png", sanitizeName(r.Reference()))
|
||||||
|
if !result.OK() {
|
||||||
|
if r, ok := r.(httpRoute); ok {
|
||||||
|
// fallback to parse html
|
||||||
|
result = findIconSlow(ctx, r, uri, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.OK() {
|
||||||
|
storeIconCache(r.Key(), result)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string) *FetchResult {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "request timeout"}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stack) > maxRedirectDepth {
|
||||||
|
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "too many redirects"}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeoutCause(ctx, faviconFetchTimeout, errors.New("favicon request timeout"))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
newReq, err := http.NewRequestWithContext(ctx, "GET", r.TargetURL().String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return &FetchResult{StatusCode: http.StatusInternalServerError, ErrMsg: "cannot create request"}
|
||||||
|
}
|
||||||
|
newReq.Header.Set("Accept-Encoding", "identity") // disable compression
|
||||||
|
|
||||||
|
u, err := url.ParseRequestURI(strutils.SanitizeURI(uri))
|
||||||
|
if err != nil {
|
||||||
|
return &FetchResult{StatusCode: http.StatusInternalServerError, ErrMsg: "cannot parse uri"}
|
||||||
|
}
|
||||||
|
newReq.URL.Path = u.Path
|
||||||
|
newReq.URL.RawPath = u.RawPath
|
||||||
|
newReq.URL.RawQuery = u.RawQuery
|
||||||
|
newReq.RequestURI = u.String()
|
||||||
|
|
||||||
|
c := newContent()
|
||||||
|
r.ServeHTTP(c, newReq)
|
||||||
|
if c.status != http.StatusOK {
|
||||||
|
switch c.status {
|
||||||
|
case 0:
|
||||||
|
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "connection error"}
|
||||||
|
default:
|
||||||
|
if loc := c.Header().Get("Location"); loc != "" {
|
||||||
|
loc = strutils.SanitizeURI(loc)
|
||||||
|
if loc == "/" || loc == newReq.URL.Path || slices.Contains(stack, loc) {
|
||||||
|
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "circular redirect"}
|
||||||
|
}
|
||||||
|
// append current path to stack
|
||||||
|
// handles redirect to the same path with different query
|
||||||
|
return findIconSlow(ctx, r, loc, append(stack, newReq.URL.Path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &FetchResult{StatusCode: c.status, ErrMsg: "upstream error: " + string(c.data)}
|
||||||
|
}
|
||||||
|
// return icon data
|
||||||
|
if !gphttp.GetContentType(c.header).IsHTML() {
|
||||||
|
return &FetchResult{Icon: c.data, contentType: c.header.Get("Content-Type")}
|
||||||
|
}
|
||||||
|
// try extract from "link[rel=icon]" from path "/"
|
||||||
|
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(c.data))
|
||||||
|
if err != nil {
|
||||||
|
return &FetchResult{StatusCode: http.StatusInternalServerError, ErrMsg: "failed to parse html"}
|
||||||
|
}
|
||||||
|
ele := doc.Find("head > link[rel=icon]").First()
|
||||||
|
if ele.Length() == 0 {
|
||||||
|
return &FetchResult{StatusCode: http.StatusNotFound, ErrMsg: "icon element not found"}
|
||||||
|
}
|
||||||
|
href := ele.AttrOr("href", "")
|
||||||
|
if href == "" {
|
||||||
|
return &FetchResult{StatusCode: http.StatusNotFound, ErrMsg: "icon href not found"}
|
||||||
|
}
|
||||||
|
// https://en.wikipedia.org/wiki/Data_URI_scheme
|
||||||
|
if strings.HasPrefix(href, "data:image/") {
|
||||||
|
dataURI, err := dataurl.DecodeString(href)
|
||||||
|
if err != nil {
|
||||||
|
return &FetchResult{StatusCode: http.StatusInternalServerError, ErrMsg: "failed to decode favicon"}
|
||||||
|
}
|
||||||
|
return &FetchResult{Icon: dataURI.Data, contentType: dataURI.ContentType()}
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(href, "http://"), strings.HasPrefix(href, "https://"):
|
||||||
|
return fetchIconAbsolute(ctx, href)
|
||||||
|
default:
|
||||||
|
return findIconSlow(ctx, r, href, append(stack, newReq.URL.Path))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestOverrideItem(t *testing.T) {
|
func TestOverrideItem(t *testing.T) {
|
||||||
InitOverridesConfig()
|
|
||||||
a := &Item{
|
a := &Item{
|
||||||
Alias: "foo",
|
Alias: "foo",
|
||||||
ItemConfig: &ItemConfig{
|
ItemConfig: &ItemConfig{
|
||||||
|
|||||||
145
internal/homepage/icon_cache.go
Normal file
145
internal/homepage/icon_cache.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package homepage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/jsonstore"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cacheEntry struct {
|
||||||
|
Icon []byte `json:"icon"`
|
||||||
|
ContentType string `json:"content_type,omitempty"`
|
||||||
|
LastAccess atomic.Value[time.Time] `json:"last_access"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache key can be absolute url or route name.
|
||||||
|
var (
|
||||||
|
iconCache = jsonstore.Store[*cacheEntry](common.NamespaceIconCache)
|
||||||
|
iconMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
iconCacheTTL = 3 * 24 * time.Hour
|
||||||
|
cleanUpInterval = time.Minute
|
||||||
|
maxIconSize = 1024 * 1024 // 1MB
|
||||||
|
maxCacheEntries = 100
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go func() {
|
||||||
|
cleanupTicker := time.NewTicker(cleanUpInterval)
|
||||||
|
defer cleanupTicker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-task.RootContextCanceled():
|
||||||
|
return
|
||||||
|
case <-cleanupTicker.C:
|
||||||
|
pruneExpiredIconCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pruneExpiredIconCache() {
|
||||||
|
nPruned := 0
|
||||||
|
for key, icon := range iconCache.Range {
|
||||||
|
if icon.IsExpired() {
|
||||||
|
iconCache.Delete(key)
|
||||||
|
nPruned++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if iconCache.Size() > maxCacheEntries {
|
||||||
|
iconCache.Clear()
|
||||||
|
newIconCache := make(map[string]*cacheEntry, maxCacheEntries)
|
||||||
|
i := 0
|
||||||
|
for key, icon := range iconCache.Range {
|
||||||
|
if i == maxCacheEntries {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !icon.IsExpired() {
|
||||||
|
newIconCache[key] = icon
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key, icon := range newIconCache {
|
||||||
|
iconCache.Store(key, icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nPruned > 0 {
|
||||||
|
logging.Info().Int("pruned", nPruned).Msg("pruned expired icon cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PruneRouteIconCache(route route) {
|
||||||
|
iconCache.Delete(route.Key())
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadIconCache(key string) *FetchResult {
|
||||||
|
iconMu.RLock()
|
||||||
|
defer iconMu.RUnlock()
|
||||||
|
icon, ok := iconCache.Load(key)
|
||||||
|
if ok && len(icon.Icon) > 0 {
|
||||||
|
logging.Debug().
|
||||||
|
Str("key", key).
|
||||||
|
Msg("icon found in cache")
|
||||||
|
icon.LastAccess.Store(utils.TimeNow())
|
||||||
|
return &FetchResult{Icon: icon.Icon, contentType: icon.ContentType}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeIconCache(key string, result *FetchResult) {
|
||||||
|
icon := result.Icon
|
||||||
|
if len(icon) > maxIconSize {
|
||||||
|
logging.Debug().Int("size", len(icon)).Msg("icon cache size exceeds max cache size")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iconMu.Lock()
|
||||||
|
defer iconMu.Unlock()
|
||||||
|
|
||||||
|
entry := &cacheEntry{Icon: icon, ContentType: result.contentType}
|
||||||
|
entry.LastAccess.Store(time.Now())
|
||||||
|
iconCache.Store(key, entry)
|
||||||
|
logging.Debug().Str("key", key).Int("size", len(icon)).Msg("stored icon cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *cacheEntry) IsExpired() bool {
|
||||||
|
return time.Since(e.LastAccess.Load()) > iconCacheTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *cacheEntry) UnmarshalJSON(data []byte) error {
|
||||||
|
var tmp struct {
|
||||||
|
Icon []byte `json:"icon"`
|
||||||
|
ContentType string `json:"content_type,omitempty"`
|
||||||
|
LastAccess time.Time `json:"last_access"`
|
||||||
|
}
|
||||||
|
// check if data is json
|
||||||
|
if json.Valid(data) {
|
||||||
|
err := json.Unmarshal(data, &tmp)
|
||||||
|
// return only if unmarshal is successful
|
||||||
|
// otherwise fallback to base64
|
||||||
|
if err == nil {
|
||||||
|
e.Icon = tmp.Icon
|
||||||
|
e.ContentType = tmp.ContentType
|
||||||
|
e.LastAccess.Store(tmp.LastAccess)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fallback to base64
|
||||||
|
icon, err := base64.StdEncoding.DecodeString(string(data))
|
||||||
|
if err == nil {
|
||||||
|
e.Icon = icon
|
||||||
|
e.LastAccess.Store(time.Now())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,10 +61,10 @@ func NewWalkXCodeIconURL(name, format string) *IconURL {
|
|||||||
// otherwise returns true.
|
// otherwise returns true.
|
||||||
func (u *IconURL) HasIcon() bool {
|
func (u *IconURL) HasIcon() bool {
|
||||||
if u.IconSource == IconSourceSelfhSt {
|
if u.IconSource == IconSourceSelfhSt {
|
||||||
return internal.HasSelfhstIcon(u.Extra.Name, u.Extra.FileType)
|
return HasSelfhstIcon(u.Extra.Name, u.Extra.FileType)
|
||||||
}
|
}
|
||||||
if u.IconSource == IconSourceWalkXCode {
|
if u.IconSource == IconSourceWalkXCode {
|
||||||
return internal.HasWalkxCodeIcon(u.Extra.Name, u.Extra.FileType)
|
return HasWalkxCodeIcon(u.Extra.Name, u.Extra.FileType)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package homepage
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
expect "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIconURL(t *testing.T) {
|
func TestIconURL(t *testing.T) {
|
||||||
@@ -114,11 +114,11 @@ func TestIconURL(t *testing.T) {
|
|||||||
u := &IconURL{}
|
u := &IconURL{}
|
||||||
err := u.Parse(tc.input)
|
err := u.Parse(tc.input)
|
||||||
if tc.wantErr {
|
if tc.wantErr {
|
||||||
ExpectError(t, ErrInvalidIconURL, err)
|
expect.ErrorIs(t, ErrInvalidIconURL, err)
|
||||||
} else {
|
} else {
|
||||||
tc.wantValue.FullValue = tc.input
|
tc.wantValue.FullValue = tc.input
|
||||||
ExpectNoError(t, err)
|
expect.NoError(t, err)
|
||||||
ExpectEqual(t, u, tc.wantValue)
|
expect.Equal(t, u, tc.wantValue)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package internal
|
package homepage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,6 +69,10 @@ func InitIconListCache() {
|
|||||||
Int("display_names", len(iconsCache.DisplayNames)).
|
Int("display_names", len(iconsCache.DisplayNames)).
|
||||||
Msg("icon list cache loaded")
|
Msg("icon list cache loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
task.OnProgramExit("save_icon_list_cache", func() {
|
||||||
|
utils.SaveJSON(common.IconListCachePath, iconsCache, 0o644)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListAvailableIcons() (*Cache, error) {
|
func ListAvailableIcons() (*Cache, error) {
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
package homepage
|
package homepage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"maps"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/jsonstore"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type OverrideConfig struct {
|
type OverrideConfig struct {
|
||||||
@@ -17,39 +16,19 @@ type OverrideConfig struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var overrideConfigInstance = &OverrideConfig{
|
var overrideConfigInstance = jsonstore.Object[*OverrideConfig](common.NamespaceHomepageOverrides)
|
||||||
ItemOverrides: make(map[string]*ItemConfig),
|
|
||||||
DisplayOrder: make(map[string]int),
|
|
||||||
CategoryOrder: make(map[string]int),
|
|
||||||
ItemVisibility: make(map[string]bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitOverridesConfig() {
|
|
||||||
overrideConfigInstance.mu.Lock()
|
|
||||||
defer overrideConfigInstance.mu.Unlock()
|
|
||||||
|
|
||||||
err := utils.LoadJSONIfExist(common.HomepageJSONConfigPath, overrideConfigInstance)
|
|
||||||
if err != nil {
|
|
||||||
logging.Error().Err(err).Msg("failed to load homepage overrides config")
|
|
||||||
} else if len(overrideConfigInstance.ItemOverrides) > 0 {
|
|
||||||
logging.Info().
|
|
||||||
Int("count", len(overrideConfigInstance.ItemOverrides)).
|
|
||||||
Msg("homepage overrides config loaded")
|
|
||||||
}
|
|
||||||
task.OnProgramExit("save_homepage_json_config", func() {
|
|
||||||
if len(overrideConfigInstance.ItemOverrides) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := utils.SaveJSON(common.HomepageJSONConfigPath, overrideConfigInstance, 0o644); err != nil {
|
|
||||||
logging.Error().Err(err).Msg("failed to save homepage overrides config")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetOverrideConfig() *OverrideConfig {
|
func GetOverrideConfig() *OverrideConfig {
|
||||||
return overrideConfigInstance
|
return overrideConfigInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *OverrideConfig) Initialize() {
|
||||||
|
c.ItemOverrides = make(map[string]*ItemConfig)
|
||||||
|
c.DisplayOrder = make(map[string]int)
|
||||||
|
c.CategoryOrder = make(map[string]int)
|
||||||
|
c.ItemVisibility = make(map[string]bool)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *OverrideConfig) OverrideItem(alias string, override *ItemConfig) {
|
func (c *OverrideConfig) OverrideItem(alias string, override *ItemConfig) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
@@ -59,9 +38,7 @@ func (c *OverrideConfig) OverrideItem(alias string, override *ItemConfig) {
|
|||||||
func (c *OverrideConfig) OverrideItems(items map[string]*ItemConfig) {
|
func (c *OverrideConfig) OverrideItems(items map[string]*ItemConfig) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
for key, value := range items {
|
maps.Copy(c.ItemOverrides, items)
|
||||||
c.ItemOverrides[key] = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OverrideConfig) GetOverride(alias string, item *ItemConfig) *ItemConfig {
|
func (c *OverrideConfig) GetOverride(alias string, item *ItemConfig) *ItemConfig {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user