mirror of
https://github.com/yusing/godoxy.git
synced 2025-08-11 10:17:01 +00:00
Compare commits
280 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fce9ce21c9 | ||
![]() |
475e697490 | ||
![]() |
68ac4f952d | ||
![]() |
a2e6688056 | ||
![]() |
e02cacdf2a | ||
![]() |
46c7ee4d84 | ||
![]() |
f39513483b | ||
![]() |
731121595c | ||
![]() |
8025af6067 | ||
![]() |
47910774dd | ||
![]() |
b6bfd19cc2 | ||
![]() |
e3b53a548d | ||
![]() |
a954ac8946 | ||
![]() |
814ff33352 | ||
![]() |
b1d5c4b091 | ||
![]() |
72dc783e23 | ||
![]() |
1c95bbba6e | ||
![]() |
0c552c9cea | ||
![]() |
5631b1540a | ||
![]() |
24f949f053 | ||
![]() |
9d712b91ff | ||
![]() |
4189ffa1db | ||
![]() |
e906b358fa | ||
![]() |
f179de9231 | ||
![]() |
1d546624de | ||
![]() |
ecc9d306d1 | ||
![]() |
5ce1c7865e | ||
![]() |
7d17a01de1 | ||
![]() |
cabb840a91 | ||
![]() |
4825f768f3 | ||
![]() |
5fdb023188 | ||
![]() |
4abf61a421 | ||
![]() |
96b7c3fcec | ||
![]() |
f8c57d930f | ||
![]() |
880d66c75e | ||
![]() |
4649c8d479 | ||
![]() |
20021b3cae | ||
![]() |
cfa9201f82 | ||
![]() |
b5328fe5e7 | ||
![]() |
25fbcc4ab9 | ||
![]() |
421aaecba4 | ||
![]() |
01773976d1 | ||
![]() |
2263d6063e | ||
![]() |
cfe0f6bb70 | ||
![]() |
a90d2b90d1 | ||
![]() |
af9629424e | ||
![]() |
ee6cf29bc1 | ||
![]() |
c4a780e061 | ||
![]() |
09c244ef3c | ||
![]() |
bd0fe36c53 | ||
![]() |
d240da4393 | ||
![]() |
9470a14fe8 | ||
![]() |
d3568d9c35 | ||
![]() |
44ef351840 | ||
![]() |
a39d527fc1 | ||
![]() |
22ab043e06 | ||
![]() |
b670cdbd49 | ||
![]() |
45e34d691a | ||
![]() |
e82480a639 | ||
![]() |
e39407886d | ||
![]() |
3135e377a9 | ||
![]() |
bdb3343a7c | ||
![]() |
b411c6d504 | ||
![]() |
966a59b5c9 | ||
![]() |
58db228e25 | ||
![]() |
e737737415 | ||
![]() |
9087c4f195 | ||
![]() |
4705989f4b | ||
![]() |
cb506120dd | ||
![]() |
88aaf956e5 | ||
![]() |
ecfd018b0b | ||
![]() |
54bf84dcba | ||
![]() |
57200bc1e9 | ||
![]() |
6f9bb410f5 | ||
![]() |
e62e667b49 | ||
![]() |
abe81541db | ||
![]() |
9e5d33714c | ||
![]() |
93a81fd558 | ||
![]() |
72923b8cfa | ||
![]() |
24ba4c2a46 | ||
![]() |
ed07bf42ce | ||
![]() |
371e756307 | ||
![]() |
32d8292b17 | ||
![]() |
717fd0e58c | ||
![]() |
2628d9e8a8 | ||
![]() |
c90795e614 | ||
![]() |
4a6bed7728 | ||
![]() |
216c03c5ff | ||
![]() |
2e9f113224 | ||
![]() |
9d58977fa6 | ||
![]() |
8469b6406c | ||
![]() |
b163771956 | ||
![]() |
c1221e61d4 | ||
![]() |
4a8bd48ad5 | ||
![]() |
ade93d49a3 | ||
![]() |
82ee75daab | ||
![]() |
f0ab14cb1e | ||
![]() |
5b7c392297 | ||
![]() |
1f1ae38e4d | ||
![]() |
22d44a6bb0 | ||
![]() |
6a5cd1266b | ||
![]() |
1cf18657b6 | ||
![]() |
63c4bdc73d | ||
![]() |
20a1649275 | ||
![]() |
0f3b8e68ce | ||
![]() |
5a3e3f19c7 | ||
![]() |
df193a42fc | ||
![]() |
f1e204f7fd | ||
![]() |
ff08c40403 | ||
![]() |
d8266f779f | ||
![]() |
9711867fbe | ||
![]() |
fc8592ab45 | ||
![]() |
3dbab118af | ||
![]() |
1f50ee7f2f | ||
![]() |
cee6eaecff | ||
![]() |
67a6b89ea5 | ||
![]() |
78be9b1c71 | ||
![]() |
26856b612a | ||
![]() |
36ceba3ae7 | ||
![]() |
f45f3fba79 | ||
![]() |
4bbff323e3 | ||
![]() |
2e68baa93e | ||
![]() |
a162371ec5 | ||
![]() |
8f9c76daa5 | ||
![]() |
8b3e058885 | ||
![]() |
023cbc81bc | ||
![]() |
b490e8c475 | ||
![]() |
8e27886235 | ||
![]() |
7435b8e485 | ||
![]() |
21724c037f | ||
![]() |
44b4cff35e | ||
![]() |
1e24765b17 | ||
![]() |
a1f2a84a16 | ||
![]() |
453262832a | ||
![]() |
99e975145c | ||
![]() |
e300170c51 | ||
![]() |
1382137f20 | ||
![]() |
54d7508f5d | ||
![]() |
71ca8c738e | ||
![]() |
f1eefde964 | ||
![]() |
84e7a6591e | ||
![]() |
30c76cfc5f | ||
![]() |
a8ba42e360 | ||
![]() |
cd291556fc | ||
![]() |
0d41809630 | ||
![]() |
53acf75c04 | ||
![]() |
cf30fe6cfc | ||
![]() |
55bbcae911 | ||
![]() |
b30c0d7dc0 | ||
![]() |
198ae2cd02 | ||
![]() |
26938eb6ed | ||
![]() |
48823a860f | ||
![]() |
985ff0a74d | ||
![]() |
43b493c60e | ||
![]() |
e0e0fab127 | ||
![]() |
fc0dbd940c | ||
![]() |
0208e6286f | ||
![]() |
2c0b68c8c2 | ||
![]() |
c05059765d | ||
![]() |
a06787593c | ||
![]() |
8fe94d6d14 | ||
![]() |
4ddfb48b9d | ||
![]() |
31dc112591 | ||
![]() |
6797897814 | ||
![]() |
99eccd0b95 | ||
![]() |
0387739b94 | ||
![]() |
ead27c72f1 | ||
![]() |
455a85e6a0 | ||
![]() |
8424fd9f1a | ||
![]() |
75ee0e63bd | ||
![]() |
1ce607029a | ||
![]() |
1e80ad2a44 | ||
![]() |
4daefa19d1 | ||
![]() |
491231e439 | ||
![]() |
c90ec8caa1 | ||
![]() |
9eb674029e | ||
![]() |
e41c6530ab | ||
![]() |
afd35c183d | ||
![]() |
f190483b4e | ||
![]() |
7b0ed09772 | ||
![]() |
4415bffc35 | ||
![]() |
ddab2766b4 | ||
![]() |
ef95682116 | ||
![]() |
dd65a8d04b | ||
![]() |
aa23b5b595 | ||
![]() |
c55c6c84bc | ||
![]() |
a45e5e17db | ||
![]() |
b8c0961de3 | ||
![]() |
62d3d200e6 | ||
![]() |
bf32cafd90 | ||
![]() |
1c182b5a7d | ||
![]() |
ad60f377ba | ||
![]() |
75db09b1f3 | ||
![]() |
6dd849f480 | ||
![]() |
e2ae29795d | ||
![]() |
92fa0f8168 | ||
![]() |
b090598b68 | ||
![]() |
2cec88d3ce | ||
![]() |
4df31263b5 | ||
![]() |
9eae809690 | ||
![]() |
f1ba554a24 | ||
![]() |
f9a8aede20 | ||
![]() |
e275ee634c | ||
![]() |
797d88772f | ||
![]() |
8ef8015a7f | ||
![]() |
5fce4b445b | ||
![]() |
7552a706a7 | ||
![]() |
e1bc6d1f44 | ||
![]() |
56850a9580 | ||
![]() |
5f780f4902 | ||
![]() |
ccb4639f43 | ||
![]() |
ac1470d81d | ||
![]() |
efaabfa63a | ||
![]() |
9043cf25c5 | ||
![]() |
98e90d7a0b | ||
![]() |
82c829de18 | ||
![]() |
2fe4fef779 | ||
![]() |
91302ceed7 | ||
![]() |
7fa7b55b18 | ||
![]() |
69ee8495d8 | ||
![]() |
28d9a72908 | ||
![]() |
770c698332 | ||
![]() |
cd4c843025 | ||
![]() |
f0cf89060b | ||
![]() |
f79a15bac6 | ||
![]() |
2b4a70a550 | ||
![]() |
f06741428c | ||
![]() |
16e6e72454 | ||
![]() |
100d2c392f | ||
![]() |
829eb08e37 | ||
![]() |
53d54a09b0 | ||
![]() |
62c551c7fe | ||
![]() |
80e59bb481 | ||
![]() |
7a5afc3612 | ||
![]() |
2c0349c11c | ||
![]() |
8e3c2cc8d4 | ||
![]() |
d35afdb3c9 | ||
![]() |
ae093ebf40 | ||
![]() |
aa8af4185b | ||
![]() |
0029cf69d6 | ||
![]() |
33e400a17e | ||
![]() |
1d22bcfed9 | ||
![]() |
978d82060e | ||
![]() |
7aa1215491 | ||
![]() |
0b69589586 | ||
![]() |
bca3cd84d1 | ||
![]() |
ce4bf2f646 | ||
![]() |
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 |
413 changed files with 18024 additions and 9477 deletions
.env.example
.github/workflows
agent-binary.ymldocker-image-nightly.ymldocker-image-prod.ymldocker-image-socket-proxy.ymldocker-image.yml
.gitignore.golangci.yml.trunk
.vscode
DockerfileLICENSEMakefileREADME.mdREADME_CHT.mdagent
cmd
compose.example.ymlconfig.example.ymlgo.modgo.suminternal
acl
api
handler.go
v1
auth
autocert
common
config
dnsproviders
31
.env.example
31
.env.example
|
@ -1,6 +1,15 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# container uid and gid (must match the owner of mounted directories)
|
||||||
|
GODOXY_UID=1000
|
||||||
|
GODOXY_GID=1000
|
||||||
|
|
||||||
|
# Set GODOXY_API_JWT_SECURE=false to allow http
|
||||||
|
GODOXY_API_JWT_SECURE=true
|
||||||
# API JWT Configuration (common)
|
# API JWT Configuration (common)
|
||||||
# generate secret with `openssl rand -base64 32`
|
# generate secret with `openssl rand -base64 32`
|
||||||
GODOXY_API_JWT_SECRET=
|
GODOXY_API_JWT_SECRET=
|
||||||
|
@ -16,12 +25,11 @@ GODOXY_API_PASSWORD=password
|
||||||
|
|
||||||
# OIDC Configuration (optional)
|
# OIDC Configuration (optional)
|
||||||
# Uncomment and configure these values to enable OIDC authentication.
|
# Uncomment and configure these values to enable OIDC authentication.
|
||||||
# For `GODOXY_OIDC_SCOPES` you may also include `offline_access` if your Idp supports it (e.g. Authentik)
|
|
||||||
#
|
#
|
||||||
# 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
|
||||||
# GODOXY_OIDC_SCOPES=openid, profile, email
|
# GODOXY_OIDC_SCOPES=openid, profile, email, groups # you may also include `offline_access` if your Idp supports it (e.g. Authentik, Pocket ID)
|
||||||
#
|
#
|
||||||
# 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:
|
||||||
|
@ -42,14 +50,29 @@ GODOXY_API_PASSWORD=password
|
||||||
GODOXY_HTTP_ADDR=:80
|
GODOXY_HTTP_ADDR=:80
|
||||||
GODOXY_HTTPS_ADDR=:443
|
GODOXY_HTTPS_ADDR=:443
|
||||||
|
|
||||||
|
# Enable HTTP3
|
||||||
|
GODOXY_HTTP3_ENABLED=true
|
||||||
|
|
||||||
# API listening address
|
# API listening address
|
||||||
GODOXY_API_ADDR=127.0.0.1:8888
|
GODOXY_API_ADDR=127.0.0.1:8888
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
GODOXY_METRICS_DISABLE_CPU=false
|
||||||
|
GODOXY_METRICS_DISABLE_MEMORY=false
|
||||||
|
GODOXY_METRICS_DISABLE_DISK=false
|
||||||
|
GODOXY_METRICS_DISABLE_NETWORK=false
|
||||||
|
GODOXY_METRICS_DISABLE_SENSORS=false
|
||||||
|
|
||||||
# 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
|
||||||
|
SOCKET_PROXY_LISTEN_ADDR=127.0.0.1:2375
|
||||||
|
|
||||||
# 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:
|
||||||
|
|
3
.github/workflows/docker-image-nightly.yml
vendored
3
.github/workflows/docker-image-nightly.yml
vendored
|
@ -15,9 +15,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
image_name: ${{ github.repository_owner }}/godoxy
|
image_name: ${{ github.repository_owner }}/godoxy
|
||||||
tag: nightly
|
tag: nightly
|
||||||
|
target: main
|
||||||
build-nightly-agent:
|
build-nightly-agent:
|
||||||
uses: ./.github/workflows/docker-image.yml
|
uses: ./.github/workflows/docker-image.yml
|
||||||
with:
|
with:
|
||||||
image_name: ${{ github.repository_owner }}/godoxy-agent
|
image_name: ${{ github.repository_owner }}/godoxy-agent
|
||||||
tag: nightly
|
tag: nightly
|
||||||
agent: true
|
target: agent
|
||||||
|
|
3
.github/workflows/docker-image-prod.yml
vendored
3
.github/workflows/docker-image-prod.yml
vendored
|
@ -12,9 +12,10 @@ jobs:
|
||||||
image_name: ${{ github.repository_owner }}/godoxy
|
image_name: ${{ github.repository_owner }}/godoxy
|
||||||
old_image_name: ${{ github.repository_owner }}/go-proxy
|
old_image_name: ${{ github.repository_owner }}/go-proxy
|
||||||
tag: latest
|
tag: latest
|
||||||
|
target: main
|
||||||
build-prod-agent:
|
build-prod-agent:
|
||||||
uses: ./.github/workflows/docker-image.yml
|
uses: ./.github/workflows/docker-image.yml
|
||||||
with:
|
with:
|
||||||
image_name: ${{ github.repository_owner }}/godoxy-agent
|
image_name: ${{ github.repository_owner }}/godoxy-agent
|
||||||
tag: latest
|
tag: latest
|
||||||
agent: true
|
target: agent
|
||||||
|
|
23
.github/workflows/docker-image-socket-proxy.yml
vendored
Normal file
23
.github/workflows/docker-image-socket-proxy.yml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
name: Docker Image CI (socket-proxy)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "socket-proxy/**"
|
||||||
|
tags-ignore:
|
||||||
|
- '**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: ./.github/workflows/docker-image.yml
|
||||||
|
with:
|
||||||
|
image_name: ${{ github.repository_owner }}/socket-proxy
|
||||||
|
tag: latest
|
||||||
|
target: socket-proxy
|
||||||
|
dockerfile: socket-proxy.Dockerfile
|
23
.github/workflows/docker-image.yml
vendored
23
.github/workflows/docker-image.yml
vendored
|
@ -12,16 +12,20 @@ on:
|
||||||
old_image_name:
|
old_image_name:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
agent:
|
target:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
dockerfile:
|
||||||
required: false
|
required: false
|
||||||
default: false
|
type: string
|
||||||
type: boolean
|
default: Dockerfile
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
MAKE_ARGS: agent=${{ inputs.agent && '1' || '0' }}
|
MAKE_ARGS: ${{ inputs.target }}=1
|
||||||
DIGEST_PATH: /tmp/digests/${{ inputs.agent && 'agent' || 'main' }}
|
DIGEST_PATH: /tmp/digests/${{ inputs.target }}
|
||||||
DIGEST_NAME_SUFFIX: ${{ inputs.agent && 'agent' || 'main' }}
|
DIGEST_NAME_SUFFIX: ${{ inputs.target }}
|
||||||
|
DOCKERFILE: ${{ inputs.dockerfile }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -76,11 +80,14 @@ jobs:
|
||||||
with:
|
with:
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
file: ${{ env.DOCKERFILE }}
|
||||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||||
cache-from: |
|
cache-from: |
|
||||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}-${{ inputs.tag }}
|
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}
|
||||||
|
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }}
|
||||||
cache-to: |
|
cache-to: |
|
||||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}-${{ inputs.tag }},mode=max
|
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max
|
||||||
|
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }},mode=max
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ github.ref_name }}
|
VERSION=${{ github.ref_name }}
|
||||||
MAKE_ARGS=${{ env.MAKE_ARGS }}
|
MAKE_ARGS=${{ env.MAKE_ARGS }}
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -30,6 +30,7 @@ todo.md
|
||||||
mtrace.json
|
mtrace.json
|
||||||
.env
|
.env
|
||||||
.cursorrules
|
.cursorrules
|
||||||
|
.cursor/
|
||||||
.windsurfrules
|
.windsurfrules
|
||||||
test.Dockerfile
|
test.Dockerfile
|
||||||
|
|
||||||
|
|
202
.golangci.yml
202
.golangci.yml
|
@ -1,43 +1,75 @@
|
||||||
run:
|
version: "2"
|
||||||
timeout: 10m
|
linters:
|
||||||
|
default: all
|
||||||
linters-settings:
|
|
||||||
govet:
|
|
||||||
enable-all: true
|
|
||||||
disable:
|
disable:
|
||||||
- shadow
|
# - bodyclose
|
||||||
- fieldalignment
|
- containedctx
|
||||||
gocyclo:
|
# - contextcheck
|
||||||
min-complexity: 14
|
- cyclop
|
||||||
misspell:
|
- depguard
|
||||||
locale: US
|
# - dupl
|
||||||
|
- err113
|
||||||
|
- exhaustive
|
||||||
|
- exhaustruct
|
||||||
|
- funcorder
|
||||||
|
- forcetypeassert
|
||||||
|
- gochecknoglobals
|
||||||
|
- gochecknoinits
|
||||||
|
- gocognit
|
||||||
|
- goconst
|
||||||
|
- gocyclo
|
||||||
|
- gomoddirectives
|
||||||
|
- gosmopolitan
|
||||||
|
- ireturn
|
||||||
|
- lll
|
||||||
|
- maintidx
|
||||||
|
- makezero
|
||||||
|
- mnd
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nlreturn
|
||||||
|
- nonamedreturns
|
||||||
|
- paralleltest
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- sqlclosecheck
|
||||||
|
- tagliatelle
|
||||||
|
- testpackage
|
||||||
|
- tparallel
|
||||||
|
- varnamelen
|
||||||
|
- wrapcheck
|
||||||
|
- wsl
|
||||||
|
settings:
|
||||||
|
errcheck:
|
||||||
|
exclude-functions:
|
||||||
|
- fmt.Fprintln
|
||||||
|
forbidigo:
|
||||||
|
forbid:
|
||||||
|
- pattern: ^print(ln)?$
|
||||||
funlen:
|
funlen:
|
||||||
lines: -1
|
lines: -1
|
||||||
statements: 120
|
statements: 120
|
||||||
forbidigo:
|
gocyclo:
|
||||||
forbid:
|
min-complexity: 14
|
||||||
- ^print(ln)?$
|
|
||||||
godox:
|
godox:
|
||||||
keywords:
|
keywords:
|
||||||
- FIXME
|
- FIXME
|
||||||
tagalign:
|
gomoddirectives:
|
||||||
align: false
|
replace-allow-list:
|
||||||
sort: true
|
- github.com/abbot/go-http-auth
|
||||||
order:
|
- github.com/gorilla/mux
|
||||||
- description
|
- github.com/mailgun/minheap
|
||||||
- json
|
- github.com/mailgun/multibuf
|
||||||
- toml
|
- github.com/jaguilar/vt100
|
||||||
- yaml
|
- github.com/cucumber/godog
|
||||||
- yml
|
- github.com/http-wasm/http-wasm-host-go
|
||||||
- label
|
govet:
|
||||||
- label-slice-as-struct
|
disable:
|
||||||
- file
|
- shadow
|
||||||
- kv
|
- fieldalignment
|
||||||
- export
|
enable-all: true
|
||||||
stylecheck:
|
misspell:
|
||||||
dot-import-whitelist:
|
locale: US
|
||||||
- github.com/yusing/go-proxy/internal/utils/testing # go tests only
|
|
||||||
- github.com/yusing/go-proxy/internal/api/v1/utils # api only
|
|
||||||
revive:
|
revive:
|
||||||
rules:
|
rules:
|
||||||
- name: struct-tag
|
- name: struct-tag
|
||||||
|
@ -67,69 +99,51 @@ linters-settings:
|
||||||
disabled: true
|
disabled: true
|
||||||
- name: unreachable-code
|
- name: unreachable-code
|
||||||
- name: redefines-builtin-id
|
- name: redefines-builtin-id
|
||||||
gomoddirectives:
|
staticcheck:
|
||||||
replace-allow-list:
|
checks:
|
||||||
- github.com/abbot/go-http-auth
|
- all
|
||||||
- github.com/gorilla/mux
|
- -SA1019
|
||||||
- github.com/mailgun/minheap
|
dot-import-whitelist:
|
||||||
- github.com/mailgun/multibuf
|
- github.com/yusing/go-proxy/internal/utils/testing
|
||||||
- github.com/jaguilar/vt100
|
- github.com/yusing/go-proxy/internal/api/v1/utils
|
||||||
- github.com/cucumber/godog
|
tagalign:
|
||||||
- github.com/http-wasm/http-wasm-host-go
|
align: false
|
||||||
|
sort: true
|
||||||
|
order:
|
||||||
|
- description
|
||||||
|
- json
|
||||||
|
- toml
|
||||||
|
- yaml
|
||||||
|
- yml
|
||||||
|
- label
|
||||||
|
- label-slice-as-struct
|
||||||
|
- file
|
||||||
|
- kv
|
||||||
|
- export
|
||||||
testifylint:
|
testifylint:
|
||||||
disable:
|
disable:
|
||||||
- suite-dont-use-pkg
|
- suite-dont-use-pkg
|
||||||
- require-error
|
- require-error
|
||||||
- go-require
|
- go-require
|
||||||
staticcheck:
|
exclusions:
|
||||||
checks:
|
generated: lax
|
||||||
- all
|
presets:
|
||||||
- -SA1019
|
- comments
|
||||||
errcheck:
|
- common-false-positives
|
||||||
exclude-functions:
|
- legacy
|
||||||
- fmt.Fprintln
|
- std-error-handling
|
||||||
linters:
|
paths:
|
||||||
enable-all: true
|
- third_party$
|
||||||
disable:
|
- builtin$
|
||||||
- execinquery # deprecated
|
- examples$
|
||||||
- gomnd # deprecated
|
formatters:
|
||||||
- sqlclosecheck # not relevant (SQL)
|
enable:
|
||||||
- rowserrcheck # not relevant (SQL)
|
- gofmt
|
||||||
- cyclop # duplicate of gocyclo
|
- gofumpt
|
||||||
- depguard # Not relevant
|
- goimports
|
||||||
- nakedret # Too strict
|
exclusions:
|
||||||
- lll # Not relevant
|
generated: lax
|
||||||
- gocyclo # must be fixed
|
paths:
|
||||||
- gocognit # Too strict
|
- third_party$
|
||||||
- nestif # Too many false-positive.
|
- builtin$
|
||||||
- prealloc # Too many false-positive.
|
- examples$
|
||||||
- makezero # Not relevant
|
|
||||||
- dupl # Too strict
|
|
||||||
- gci # I don't care
|
|
||||||
- goconst # Too annoying
|
|
||||||
- gosec # Too strict
|
|
||||||
- gochecknoinits
|
|
||||||
- gochecknoglobals
|
|
||||||
- wsl # Too strict
|
|
||||||
- nlreturn # Not relevant
|
|
||||||
- mnd # Too strict
|
|
||||||
- testpackage # Too strict
|
|
||||||
- tparallel # Not relevant
|
|
||||||
- paralleltest # Not relevant
|
|
||||||
- exhaustive # Not relevant
|
|
||||||
- exhaustruct # Not relevant
|
|
||||||
- err113 # Too strict
|
|
||||||
- wrapcheck # Too strict
|
|
||||||
- noctx # Too strict
|
|
||||||
- bodyclose # too many false-positive
|
|
||||||
- forcetypeassert # Too strict
|
|
||||||
- tagliatelle # Too strict
|
|
||||||
- varnamelen # Not relevant
|
|
||||||
- nilnil # Not relevant
|
|
||||||
- ireturn # Not relevant
|
|
||||||
- contextcheck # too many false-positive
|
|
||||||
- containedctx # too many false-positive
|
|
||||||
- maintidx # kind of duplicate of gocyclo
|
|
||||||
- nonamedreturns # Too strict
|
|
||||||
- gosmopolitan # not relevant
|
|
||||||
- exportloopref # Not relevant since go1.22
|
|
||||||
|
|
|
@ -2,36 +2,37 @@
|
||||||
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
|
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
|
||||||
version: 0.1
|
version: 0.1
|
||||||
cli:
|
cli:
|
||||||
version: 1.22.10
|
version: 1.22.15
|
||||||
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
|
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
|
||||||
plugins:
|
plugins:
|
||||||
sources:
|
sources:
|
||||||
- id: trunk
|
- id: trunk
|
||||||
ref: v1.6.7
|
ref: v1.6.8
|
||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
||||||
runtimes:
|
runtimes:
|
||||||
enabled:
|
enabled:
|
||||||
- node@18.20.5
|
- node@18.20.5
|
||||||
- python@3.10.8
|
- python@3.10.8
|
||||||
- go@1.23.2
|
- go@1.24.3
|
||||||
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
||||||
lint:
|
lint:
|
||||||
disabled:
|
disabled:
|
||||||
- markdownlint
|
- markdownlint
|
||||||
- yamllint
|
- yamllint
|
||||||
enabled:
|
enabled:
|
||||||
|
- checkov@3.2.432
|
||||||
|
- golangci-lint2@2.1.6
|
||||||
- hadolint@2.12.1-beta
|
- hadolint@2.12.1-beta
|
||||||
- actionlint@1.7.7
|
- actionlint@1.7.7
|
||||||
- git-diff-check
|
- git-diff-check
|
||||||
- gofmt@1.20.4
|
- gofmt@1.20.4
|
||||||
- golangci-lint@1.64.5
|
- osv-scanner@2.0.2
|
||||||
- osv-scanner@1.9.2
|
- oxipng@9.1.5
|
||||||
- oxipng@9.1.4
|
- prettier@3.5.3
|
||||||
- prettier@3.5.1
|
|
||||||
- shellcheck@0.10.0
|
- shellcheck@0.10.0
|
||||||
- shfmt@3.6.0
|
- shfmt@3.6.0
|
||||||
- trufflehog@3.88.9
|
- trufflehog@3.88.33
|
||||||
actions:
|
actions:
|
||||||
disabled:
|
disabled:
|
||||||
- trunk-announce
|
- trunk-announce
|
||||||
|
|
4
.vscode/settings.example.json
vendored
4
.vscode/settings.example.json
vendored
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"yaml.schemas": {
|
"yaml.schemas": {
|
||||||
"https://github.com/yusing/go-proxy/raw/main/schemas/config.schema.json": [
|
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/config.schema.json": [
|
||||||
"config.example.yml",
|
"config.example.yml",
|
||||||
"config.yml"
|
"config.yml"
|
||||||
],
|
],
|
||||||
"https://github.com/yusing/go-proxy/raw/main/schemas/routes.schema.json": [
|
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/routes.schema.json": [
|
||||||
"providers.example.yml"
|
"providers.example.yml"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
26
Dockerfile
26
Dockerfile
|
@ -1,29 +1,33 @@
|
||||||
# Stage 1: deps
|
# Stage 1: deps
|
||||||
FROM golang:1.24.2-alpine AS deps
|
FROM golang:1.24.5-alpine AS deps
|
||||||
HEALTHCHECK NONE
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
# package version does not matter
|
# package version does not matter
|
||||||
# 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
|
||||||
|
|
||||||
|
ENV GOPATH=/root/go
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# Only copy go.mod and go.sum initially for better caching
|
COPY go.mod go.sum ./
|
||||||
COPY go.mod go.sum /src/
|
|
||||||
|
|
||||||
ENV GOPATH=/root/go
|
# remove godoxy stuff from go.mod first
|
||||||
RUN go mod download -x
|
RUN sed -i '/^module github\.com\/yusing\/go-proxy/!{/github\.com\/yusing\/go-proxy/d}' go.mod && \
|
||||||
|
go mod download -x
|
||||||
|
|
||||||
# Stage 2: builder
|
# Stage 2: builder
|
||||||
FROM deps AS builder
|
FROM deps AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
COPY Makefile ./
|
COPY Makefile ./
|
||||||
COPY cmd ./cmd
|
COPY cmd ./cmd
|
||||||
COPY internal ./internal
|
COPY internal ./internal
|
||||||
COPY pkg ./pkg
|
COPY pkg ./pkg
|
||||||
COPY agent ./agent
|
COPY agent ./agent
|
||||||
|
COPY socket-proxy ./socket-proxy
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
ENV VERSION=${VERSION}
|
ENV VERSION=${VERSION}
|
||||||
|
@ -33,9 +37,10 @@ ENV MAKE_ARGS=${MAKE_ARGS}
|
||||||
|
|
||||||
ENV GOCACHE=/root/.cache/go-build
|
ENV GOCACHE=/root/.cache/go-build
|
||||||
ENV GOPATH=/root/go
|
ENV GOPATH=/root/go
|
||||||
RUN make ${MAKE_ARGS} build link-binary && \
|
|
||||||
mv bin /app/ && \
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
mkdir -p /app/error_pages /app/certs
|
--mount=type=cache,target=/root/go/pkg/mod \
|
||||||
|
make ${MAKE_ARGS} docker=1 build
|
||||||
|
|
||||||
# Stage 3: Final image
|
# Stage 3: Final image
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
@ -47,10 +52,7 @@ LABEL proxy.exclude=1
|
||||||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
|
||||||
# copy binary
|
# copy binary
|
||||||
COPY --from=builder /app /app
|
COPY --from=builder /app/run /app/run
|
||||||
|
|
||||||
# copy example config
|
|
||||||
COPY config.example.yml /app/config/config.yml
|
|
||||||
|
|
||||||
# copy certs
|
# copy certs
|
||||||
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||||
|
|
26
LICENSE
26
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 [fullname]
|
Copyright (c) 2024 - present Yusing
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -19,3 +19,27 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
internal/net/gphttp/reverseproxy/reverse_proxy_mod.go is copied from et/http/httputil/reverseproxy.go with modifications to adapt to this project.
|
||||||
|
|
||||||
|
Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
Use of this source code is governed by a BSD-style
|
||||||
|
license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
internal/utils/io.go has a modified version of io.Copy with context and HTTP flusher handling.
|
||||||
|
|
||||||
|
Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
Use of this source code is governed by a BSD-style
|
||||||
|
license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
internal/utils/strutils/split_join.go is copied from strings.Split and strings.Join with modifications to adapt to this project.
|
||||||
|
|
||||||
|
Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
Use of this source code is governed by a BSD-style
|
||||||
|
license that can be found in the LICENSE file.
|
||||||
|
|
70
Makefile
70
Makefile
|
@ -1,3 +1,4 @@
|
||||||
|
shell := /bin/sh
|
||||||
export VERSION ?= $(shell git describe --tags --abbrev=0)
|
export VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||||
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
|
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
|
||||||
export GOOS = linux
|
export GOOS = linux
|
||||||
|
@ -7,10 +8,13 @@ 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
|
PWD = ${shell pwd}/agent
|
||||||
|
else ifeq ($(socket-proxy), 1)
|
||||||
|
NAME = godoxy-socket-proxy
|
||||||
|
PWD = ${shell pwd}/socket-proxy
|
||||||
else
|
else
|
||||||
NAME = godoxy
|
NAME = godoxy
|
||||||
CMD_PATH = ./cmd
|
PWD = ${shell pwd}
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(trace), 1)
|
ifeq ($(trace), 1)
|
||||||
|
@ -25,9 +29,9 @@ ifeq ($(race), 1)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(debug), 1)
|
ifeq ($(debug), 1)
|
||||||
CGO_ENABLED = 0
|
CGO_ENABLED = 1
|
||||||
GODOXY_DEBUG = 1
|
GODOXY_DEBUG = 1
|
||||||
BUILD_FLAGS += -gcflags=all='-N -l' -tags debug
|
BUILD_FLAGS += -gcflags=all='-N -l' -tags debug -asan
|
||||||
else ifeq ($(pprof), 1)
|
else 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
|
||||||
|
@ -40,9 +44,9 @@ else
|
||||||
endif
|
endif
|
||||||
|
|
||||||
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
|
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
|
||||||
|
BIN_PATH := $(shell pwd)/bin/${NAME}
|
||||||
|
|
||||||
export NAME
|
export NAME
|
||||||
export CMD_PATH
|
|
||||||
export CGO_ENABLED
|
export CGO_ENABLED
|
||||||
export GODOXY_DEBUG
|
export GODOXY_DEBUG
|
||||||
export GODOXY_TRACE
|
export GODOXY_TRACE
|
||||||
|
@ -56,30 +60,63 @@ else
|
||||||
SETCAP_CMD = sudo setcap
|
SETCAP_CMD = sudo setcap
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
|
||||||
|
POST_BUILD = $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH};
|
||||||
|
ifeq ($(docker), 1)
|
||||||
|
POST_BUILD += mkdir -p /app && mv ${BIN_PATH} /app/run;
|
||||||
|
endif
|
||||||
|
|
||||||
.PHONY: debug
|
.PHONY: debug
|
||||||
|
|
||||||
test:
|
test:
|
||||||
GODOXY_TEST=1 go test ./internal/...
|
GODOXY_TEST=1 go test ./internal/...
|
||||||
|
|
||||||
get:
|
docker-build-test:
|
||||||
go get -u ./cmd && go mod tidy
|
docker build -t godoxy .
|
||||||
|
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
|
||||||
|
|
||||||
|
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
|
||||||
|
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
|
||||||
|
gomod_paths := $(shell find . -name go.mod -type f | xargs dirname)
|
||||||
|
|
||||||
|
update-go:
|
||||||
|
for file in ${files}; do \
|
||||||
|
echo "updating $$file"; \
|
||||||
|
sed -i 's|go \([0-9]\+\.[0-9]\+\.[0-9]\+\)|go ${go_ver}|g' $$file; \
|
||||||
|
sed -i 's|FROM golang:.*-alpine|FROM golang:${go_ver}-alpine|g' $$file; \
|
||||||
|
done
|
||||||
|
for path in ${gomod_paths}; do \
|
||||||
|
echo "go mod tidy $$path"; \
|
||||||
|
cd ${PWD}/$$path && go mod tidy; \
|
||||||
|
done
|
||||||
|
|
||||||
|
update-deps:
|
||||||
|
for path in ${gomod_paths}; do \
|
||||||
|
echo "go get -u $$path"; \
|
||||||
|
cd ${PWD}/$$path && go get -u ./... && go mod tidy; \
|
||||||
|
done
|
||||||
|
|
||||||
|
mod-tidy:
|
||||||
|
for path in ${gomod_paths}; do \
|
||||||
|
echo "go mod tidy $$path"; \
|
||||||
|
cd ${PWD}/$$path && go mod tidy; \
|
||||||
|
done
|
||||||
|
|
||||||
build:
|
build:
|
||||||
mkdir -p bin
|
mkdir -p $(shell dirname ${BIN_PATH})
|
||||||
go build ${BUILD_FLAGS} -o bin/${NAME} ${CMD_PATH}
|
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
|
||||||
|
${POST_BUILD}
|
||||||
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
|
|
||||||
$(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep bin/${NAME}
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
[ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ${CMD_PATH}
|
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
|
||||||
|
|
||||||
debug:
|
debug:
|
||||||
make NAME="godoxy-test" debug=1 build
|
make NAME="godoxy-test" debug=1 build
|
||||||
sh -c 'HTTP_ADDR=:81 HTTPS_ADDR=:8443 API_ADDR=:8899 DEBUG=1 bin/godoxy-test'
|
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_PATH} debug-ls-mtrace > mtrace.json
|
||||||
|
|
||||||
rapid-crash:
|
rapid-crash:
|
||||||
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
||||||
|
@ -94,10 +131,7 @@ ci-test:
|
||||||
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
|
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
|
||||||
|
|
||||||
cloc:
|
cloc:
|
||||||
cloc --not-match-f '_test.go$$' cmd internal pkg
|
cloc --include-lang=Go --not-match-f '_test.go$$' .
|
||||||
|
|
||||||
link-binary:
|
|
||||||
ln -s /app/${NAME} bin/run
|
|
||||||
|
|
||||||
push-github:
|
push-github:
|
||||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
36
README.md
36
README.md
|
@ -2,17 +2,21 @@
|
||||||
|
|
||||||
# GoDoxy
|
# GoDoxy
|
||||||
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_godoxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||

|

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

|

|
||||||
[](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 reverse proxy with WebUI.
|
||||||
|
|
||||||
For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki)**
|
<h5>
|
||||||
|
<a href="https://docs.godoxy.dev">Website</a> | <a href="https://docs.godoxy.dev/Home.html">Wiki</a> | <a href="https://discord.gg/umReR62nRd">Discord</a>
|
||||||
|
</h5>
|
||||||
|
|
||||||
**EN** | <a href="README_CHT.md">中文</a>
|
<h5>EN | <a href="README_CHT.md">中文</a></h5>
|
||||||
|
|
||||||
|
Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)! (Thanks to [@ismesid](https://github.com/arevindh))
|
||||||
|
|
||||||
<img src="screenshots/webui.jpg" style="max-width: 650">
|
<img src="screenshots/webui.jpg" style="max-width: 650">
|
||||||
|
|
||||||
|
@ -38,15 +42,15 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
|
||||||
|
|
||||||
## Running demo
|
## Running demo
|
||||||
|
|
||||||
<https://godoxy.demo.6uo.me>
|
<https://demo.godoxy.dev>
|
||||||
|
|
||||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- **Simple**
|
- **Simple**
|
||||||
- Effortless configuration with [simple labels](https://github.com/yusing/godoxy/wiki/Docker-labels-and-Route-Files) or WebUI
|
- Effortless configuration with [simple labels](https://docs.godoxy.dev/Docker-labels-and-Route-Files) or WebUI
|
||||||
- [Simple multi-node setup](https://github.com/yusing/godoxy/wiki/Configurations#multi-docker-nodes-setup)
|
- [Simple multi-node setup](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
|
||||||
- Detailed error messages for easy troubleshooting.
|
- Detailed error messages for easy troubleshooting.
|
||||||
- **ACL**: connection / request level access control
|
- **ACL**: connection / request level access control
|
||||||
- IP/CIDR
|
- IP/CIDR
|
||||||
|
@ -54,7 +58,7 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
|
||||||
- Timezone **(Maxmind account required)**
|
- Timezone **(Maxmind account required)**
|
||||||
- **Access logging**
|
- **Access logging**
|
||||||
- **Advanced Automation**
|
- **Advanced Automation**
|
||||||
- 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))
|
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://docs.godoxy.dev/DNS-01-Providers))
|
||||||
- Auto-configuration for Docker containers
|
- Auto-configuration for Docker containers
|
||||||
- Hot-reloading of configurations and container state changes
|
- Hot-reloading of configurations and container state changes
|
||||||
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
||||||
|
@ -65,8 +69,8 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
|
||||||
- TCP/UDP port forwarding
|
- TCP/UDP port forwarding
|
||||||
- **OpenID Connect support**: SSO and secure your apps easily
|
- **OpenID Connect support**: SSO and secure your apps easily
|
||||||
- **Customization**
|
- **Customization**
|
||||||
- [HTTP middlewares](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
- [HTTP middlewares](https://docs.godoxy.dev/Middlewares)
|
||||||
- [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
- [Custom error pages support](https://docs.godoxy.dev/Custom-Error-Pages)
|
||||||
- **Web UI**
|
- **Web UI**
|
||||||
- App Dashboard
|
- App Dashboard
|
||||||
- Config Editor
|
- Config Editor
|
||||||
|
@ -99,7 +103,13 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
||||||
/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. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
3. Start the docker compose service from generated `compose.yml`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
||||||
|
|
||||||
## How does GoDoxy work
|
## How does GoDoxy work
|
||||||
|
|
||||||
|
|
|
@ -5,14 +5,18 @@
|
||||||
[](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),且帶有主頁和配置面板的反向代理
|
輕量、易用、 高效能,且帶有主頁和配置面板的反向代理
|
||||||
|
|
||||||
完整文檔請查閱 **[Wiki](https://github.com/yusing/godoxy/wiki)**(暫未有中文翻譯)
|
<h5>
|
||||||
|
<a href="https://docs.godoxy.dev">網站</a> | <a href="https://docs.godoxy.dev/Home.html">文檔</a> | <a href="https://discord.gg/umReR62nRd">Discord</a>
|
||||||
|
</h5>
|
||||||
|
|
||||||
<a href="README.md">EN</a> | **中文**
|
<h5><a href="README.md">EN</a> | 中文</h5>
|
||||||
|
|
||||||
|
有疑問? 問 [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)!(鳴謝 [@ismesid](https://github.com/arevindh))
|
||||||
|
|
||||||
<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">
|
||||||
|
|
||||||
|
@ -37,26 +41,44 @@
|
||||||
|
|
||||||
## 運行示例
|
## 運行示例
|
||||||
|
|
||||||
<https://godoxy.demo.6uo.me>
|
<https://demo.godoxy.dev>
|
||||||
|
|
||||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||||
|
|
||||||
## 主要特點
|
## 主要特點
|
||||||
|
|
||||||
- 容易使用
|
- **簡單易用**
|
||||||
- 輕鬆配置
|
- 透過 Docker[標籤](https://docs.godoxy.dev/Docker-labels-and-Route-Files)或 WebUI 輕鬆設定
|
||||||
- 簡單的多節點設置
|
- [簡單的多節點設置](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
|
||||||
- 錯誤訊息清晰詳細,易於排除故障
|
- 詳細的錯誤訊息,便於故障排除
|
||||||
- 自動 SSL 憑證管理(參見 [支援的 DNS-01 驗證提供商](https://github.com/yusing/godoxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
- **存取控制 (ACL)**:連線/請求層級存取控制
|
||||||
- 自動配置 Docker 容器
|
- IP/CIDR
|
||||||
- 容器狀態/配置文件變更時自動熱重載
|
- 國家 **(需要 Maxmind 帳戶)**
|
||||||
- **閒置休眠**:在閒置時停止容器,有流量時喚醒(_可選,參見[截圖](#閒置休眠)_)
|
- 時區 **(需要 Maxmind 帳戶)**
|
||||||
- OpenID Connect:輕鬆實現單點登入
|
- **存取日誌記錄**
|
||||||
- HTTP(s) 反向代理和TCP 和 UDP 埠轉發
|
- **自動化**
|
||||||
- [HTTP 中介軟體](https://github.com/yusing/godoxy/wiki/Middlewares) 和 [自定義錯誤頁面](https://github.com/yusing/godoxy/wiki/Middlewares#custom-error-pages)
|
- 使用 Let's Encrypt 自動管理 SSL 憑證 ([使用 DNS-01 驗證](https://docs.godoxy.dev/DNS-01-Providers))
|
||||||
- **網頁介面,具有應用儀表板和配置編輯器**
|
- Docker 容器自動配置
|
||||||
- 支援 linux/amd64、linux/arm64
|
- 設定檔與容器狀態變更時自動熱重載
|
||||||
- 使用 **[Go](https://go.dev)** 編寫
|
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
|
||||||
|
- Docker 容器
|
||||||
|
- Proxmox LXC 容器
|
||||||
|
- **流量管理**
|
||||||
|
- HTTP 反向代理
|
||||||
|
- TCP/UDP 連接埠轉送
|
||||||
|
- **OpenID Connect 支援**:輕鬆實現單點登入 (SSO) 並保護您的應用程式
|
||||||
|
- **客製化**
|
||||||
|
- [HTTP 中介軟體](https://docs.godoxy.dev/Middlewares)
|
||||||
|
- [支援自訂錯誤頁面](https://docs.godoxy.dev/Custom-Error-Pages)
|
||||||
|
- **網頁使用者介面 (Web UI)**
|
||||||
|
- 應用程式一覽
|
||||||
|
- 設定編輯器
|
||||||
|
- 執行時間與系統指標
|
||||||
|
- Docker 日誌檢視器
|
||||||
|
- **跨平台支援**
|
||||||
|
- 支援 **linux/amd64** 與 **linux/arm64**
|
||||||
|
- **高效能**
|
||||||
|
- 以 **[Go](https://go.dev)** 語言編寫
|
||||||
|
|
||||||
[🔼 回到頂部](#目錄)
|
[🔼 回到頂部](#目錄)
|
||||||
|
|
||||||
|
|
|
@ -3,20 +3,26 @@ package main
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"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"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/server"
|
"github.com/yusing/go-proxy/agent/pkg/server"
|
||||||
"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/memlogger"
|
|
||||||
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
||||||
|
httpServer "github.com/yusing/go-proxy/internal/net/gphttp/server"
|
||||||
"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"
|
||||||
|
socketproxy "github.com/yusing/go-proxy/socketproxy/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
writer := zerolog.ConsoleWriter{
|
||||||
|
Out: os.Stderr,
|
||||||
|
TimeFormat: "01-02 15:04",
|
||||||
|
}
|
||||||
|
zerolog.TimeFieldFormat = writer.TimeFormat
|
||||||
|
log.Logger = zerolog.New(writer).Level(zerolog.InfoLevel).With().Timestamp().Logger()
|
||||||
ca := &agent.PEMPair{}
|
ca := &agent.PEMPair{}
|
||||||
err := ca.Load(env.AgentCACert)
|
err := ca.Load(env.AgentCACert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -37,11 +43,11 @@ func main() {
|
||||||
gperr.LogFatal("init SSL error", err)
|
gperr.LogFatal("init SSL error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logging.Info().Msgf("GoDoxy Agent version %s", pkg.GetVersion())
|
log.Info().Msgf("GoDoxy Agent version %s", pkg.GetVersion())
|
||||||
logging.Info().Msgf("Agent name: %s", env.AgentName)
|
log.Info().Msgf("Agent name: %s", env.AgentName)
|
||||||
logging.Info().Msgf("Agent port: %d", env.AgentPort)
|
log.Info().Msgf("Agent port: %d", env.AgentPort)
|
||||||
|
|
||||||
logging.Info().Msg(`
|
log.Info().Msg(`
|
||||||
Tips:
|
Tips:
|
||||||
1. To change the agent name, you can set the AGENT_NAME environment variable.
|
1. To change the agent name, you can set the AGENT_NAME environment variable.
|
||||||
2. To change the agent port, you can set the AGENT_PORT environment variable.
|
2. To change the agent port, you can set the AGENT_PORT environment variable.
|
||||||
|
@ -55,6 +61,17 @@ Tips:
|
||||||
}
|
}
|
||||||
|
|
||||||
server.StartAgentServer(t, opts)
|
server.StartAgentServer(t, opts)
|
||||||
|
|
||||||
|
if socketproxy.ListenAddr != "" {
|
||||||
|
log.Info().Msgf("Docker socket listening on: %s", socketproxy.ListenAddr)
|
||||||
|
opts := httpServer.Options{
|
||||||
|
Name: "docker",
|
||||||
|
HTTPAddr: socketproxy.ListenAddr,
|
||||||
|
Handler: socketproxy.NewHandler(),
|
||||||
|
}
|
||||||
|
httpServer.StartServer(t, opts)
|
||||||
|
}
|
||||||
|
|
||||||
systeminfo.Poller.Start()
|
systeminfo.Poller.Start()
|
||||||
|
|
||||||
task.WaitExit(3)
|
task.WaitExit(3)
|
||||||
|
|
87
agent/go.mod
Normal file
87
agent/go.mod
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
module github.com/yusing/go-proxy/agent
|
||||||
|
|
||||||
|
go 1.24.5
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy => ..
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy/socketproxy => ../socket-proxy
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy/internal/utils => ../internal/utils
|
||||||
|
|
||||||
|
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250523125835-a2474a6ebe30
|
||||||
|
|
||||||
|
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250607110153-34d627ba1b5d
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
github.com/pion/dtls/v3 v3.0.6
|
||||||
|
github.com/puzpuzpuz/xsync/v4 v4.1.0
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
github.com/yusing/go-proxy v0.16.1
|
||||||
|
github.com/yusing/go-proxy/internal/utils v0.0.0
|
||||||
|
github.com/yusing/go-proxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||||
|
)
|
||||||
|
|
||||||
|
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/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/cli v28.3.2+incompatible // indirect
|
||||||
|
github.com/docker/docker v28.3.2+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.4 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 // 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.27.0 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
|
github.com/gotify/server/v2 v2.6.3 // indirect
|
||||||
|
github.com/kr/pretty v0.1.0 // 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/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // 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/pion/logging v0.2.4 // indirect
|
||||||
|
github.com/pion/transport/v3 v3.0.7 // 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/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
|
github.com/samber/lo v1.51.0 // indirect
|
||||||
|
github.com/samber/slog-common v0.19.0 // indirect
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.6 // 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/vincent-petithory/dataurl v1.0.0 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.2 // indirect
|
||||||
|
golang.org/x/crypto v0.40.0 // indirect
|
||||||
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
|
golang.org/x/net v0.42.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
|
golang.org/x/text v0.27.0 // indirect
|
||||||
|
golang.org/x/time v0.12.0 // indirect
|
||||||
|
golang.org/x/tools v0.35.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
292
agent/go.sum
Normal file
292
agent/go.sum
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
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/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
|
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-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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/docker/cli v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY=
|
||||||
|
github.com/docker/cli v28.3.2+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.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||||
|
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
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-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/godoxy-app/docker v0.0.0-20250523125835-a2474a6ebe30 h1:+5pYG8clUrZbUDP+x149jkRfYAGaNpAXOwut0jluoYA=
|
||||||
|
github.com/godoxy-app/docker v0.0.0-20250523125835-a2474a6ebe30/go.mod h1:7VkicOZ3VrlxOe/EP/8uwsWLGKI2wt2MV7CgxTDIYgA=
|
||||||
|
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250607110153-34d627ba1b5d h1:kih+Q38BQKBsbQmv7mZb7OP3YuSPN78ENTZUxCu4T6M=
|
||||||
|
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250607110153-34d627ba1b5d/go.mod h1:2nclxpbWQUvbTR33HI8Z/RXUG4SaF67X/pMaI/fUMa8=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
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/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/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
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.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
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/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/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/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/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||||
|
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||||
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
|
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/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/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
|
||||||
|
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||||
|
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.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||||
|
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.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
|
||||||
|
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
|
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||||
|
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||||
|
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/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/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
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/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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
|
||||||
|
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/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
|
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||||
|
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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
|
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.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||||
|
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||||
|
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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
|
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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.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-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-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.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
|
golang.org/x/sys v0.34.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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||||
|
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||||
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
|
golang.org/x/time v0.12.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.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||||
|
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||||
|
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-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 h1:qJW29YvkiJmXOYMu5Tf8lyrTp3dOS+K4z6IixtLaCf8=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
|
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||||
|
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||||
|
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-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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=
|
57
agent/pkg/agent/agent_pool.go
Normal file
57
agent/pkg/agent/agent_pool.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
|
)
|
||||||
|
|
||||||
|
var agentPool = functional.NewMapOf[string, *AgentConfig]()
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if common.IsTest {
|
||||||
|
agentPool.Store("test-agent", &AgentConfig{
|
||||||
|
Addr: "test-agent",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAgent(agentAddrOrDockerHost string) (*AgentConfig, bool) {
|
||||||
|
if !IsDockerHostAgent(agentAddrOrDockerHost) {
|
||||||
|
return getAgentByAddr(agentAddrOrDockerHost)
|
||||||
|
}
|
||||||
|
return getAgentByAddr(GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAgentByName(name string) (*AgentConfig, bool) {
|
||||||
|
for _, agent := range agentPool.Range {
|
||||||
|
if agent.Name() == name {
|
||||||
|
return agent, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddAgent(agent *AgentConfig) {
|
||||||
|
agentPool.Store(agent.Addr, agent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveAgent(agent *AgentConfig) {
|
||||||
|
agentPool.Delete(agent.Addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveAllAgents() {
|
||||||
|
agentPool.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListAgents() []*AgentConfig {
|
||||||
|
agents := make([]*AgentConfig, 0, agentPool.Size())
|
||||||
|
for _, agent := range agentPool.Range {
|
||||||
|
agents = append(agents, agent)
|
||||||
|
}
|
||||||
|
return agents
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAgentByAddr(addr string) (agent *AgentConfig, ok bool) {
|
||||||
|
agent, ok = agentPool.Load(addr)
|
||||||
|
return
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ var (
|
||||||
AGENT_PORT="{{.Port}}" \
|
AGENT_PORT="{{.Port}}" \
|
||||||
AGENT_CA_CERT="{{.CACert}}" \
|
AGENT_CA_CERT="{{.CACert}}" \
|
||||||
AGENT_SSL_CERT="{{.SSLCert}}" \
|
AGENT_SSL_CERT="{{.SSLCert}}" \
|
||||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/go-proxy/main/scripts/install-agent.sh)"`
|
bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/install-agent.sh)"`
|
||||||
installScriptTemplate = template.Must(template.New("install.sh").Parse(installScript))
|
installScriptTemplate = template.Must(template.New("install.sh").Parse(installScript))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -5,19 +5,18 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/certs"
|
"github.com/yusing/go-proxy/agent/pkg/certs"
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
|
||||||
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/pkg"
|
"github.com/yusing/go-proxy/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,6 +26,7 @@ type AgentConfig struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
name string
|
name string
|
||||||
|
version string
|
||||||
l zerolog.Logger
|
l zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,15 +80,9 @@ func (cfg *AgentConfig) Parse(addr string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func withoutBuildTime(version string) string {
|
var serverVersion = pkg.GetVersion()
|
||||||
return strings.Split(version, "-")[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkVersion(a, b string) bool {
|
func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte) error {
|
||||||
return withoutBuildTime(a) == withoutBuildTime(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
return err
|
return err
|
||||||
|
@ -90,7 +92,7 @@ func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte)
|
||||||
caCertPool := x509.NewCertPool()
|
caCertPool := x509.NewCertPool()
|
||||||
ok := caCertPool.AppendCertsFromPEM(ca)
|
ok := caCertPool.AppendCertsFromPEM(ca)
|
||||||
if !ok {
|
if !ok {
|
||||||
return gperr.New("invalid ca certificate")
|
return errors.New("invalid ca certificate")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.tlsConfig = &tls.Config{
|
cfg.tlsConfig = &tls.Config{
|
||||||
|
@ -102,21 +104,9 @@ func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte)
|
||||||
// create transport and http client
|
// create transport and http client
|
||||||
cfg.httpClient = cfg.NewHTTPClient()
|
cfg.httpClient = cfg.NewHTTPClient()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(parent.Context(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 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,29 +114,43 @@ 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()
|
|
||||||
|
|
||||||
logging.Info().Msgf("agent %q initialized", cfg.name)
|
cfg.l = log.With().Str("agent", cfg.name).Logger()
|
||||||
|
|
||||||
|
// check agent version
|
||||||
|
agentVersionBytes, _, err := cfg.Fetch(ctx, EndpointVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.version = string(agentVersionBytes)
|
||||||
|
agentVersion := pkg.ParseVersion(cfg.version)
|
||||||
|
|
||||||
|
if serverVersion.IsNewerMajorThan(agentVersion) {
|
||||||
|
log.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.name, serverVersion, agentVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msgf("agent %q initialized", cfg.name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) Start(parent task.Parent) gperr.Error {
|
func (cfg *AgentConfig) Start(ctx context.Context) error {
|
||||||
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
|
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
|
||||||
if !ok {
|
if !ok {
|
||||||
return gperr.New("invalid agent host").Subject(cfg.Addr)
|
return fmt.Errorf("invalid agent host: %s", cfg.Addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
certData, err := os.ReadFile(filepath)
|
certData, err := os.ReadFile(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gperr.Wrap(err, "failed to read agent certs")
|
return fmt.Errorf("failed to read agent certs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ca, crt, key, err := certs.ExtractCert(certData)
|
ca, crt, key, err := certs.ExtractCert(certData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gperr.Wrap(err, "failed to extract agent certs")
|
return fmt.Errorf("failed to extract agent certs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return gperr.Wrap(cfg.StartWithCerts(parent, ca, crt, key))
|
return cfg.StartWithCerts(ctx, ca, crt, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) NewHTTPClient() *http.Client {
|
func (cfg *AgentConfig) NewHTTPClient() *http.Client {
|
||||||
|
@ -170,8 +174,10 @@ func (cfg *AgentConfig) Transport() *http.Transport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var dialer = &net.Dialer{Timeout: 5 * time.Second}
|
||||||
|
|
||||||
func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
|
func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
|
||||||
return gphttp.DefaultDialer.DialContext(ctx, "tcp", cfg.Addr)
|
return dialer.DialContext(ctx, "tcp", cfg.Addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) Name() string {
|
func (cfg *AgentConfig) Name() string {
|
||||||
|
@ -186,5 +192,6 @@ func (cfg *AgentConfig) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(map[string]string{
|
return json.Marshal(map[string]string{
|
||||||
"name": cfg.Name(),
|
"name": cfg.Name(),
|
||||||
"addr": cfg.Addr,
|
"addr": cfg.Addr,
|
||||||
|
"version": cfg.version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
|
@ -12,20 +11,37 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CertsDNSName = "godoxy.agent"
|
CertsDNSName = "godoxy.agent"
|
||||||
KeySize = 2048
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func toPEMPair(certDER []byte, key *rsa.PrivateKey) *PEMPair {
|
func toPEMPair(certDER []byte, key *ecdsa.PrivateKey) *PEMPair {
|
||||||
|
marshaledKey, err := marshalECPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
// This is a critical internal error during PEM encoding of a newly generated key.
|
||||||
|
// Panicking is acceptable here as it indicates a fundamental issue.
|
||||||
|
panic(fmt.Sprintf("failed to marshal EC private key for PEM encoding: %v", err))
|
||||||
|
}
|
||||||
return &PEMPair{
|
return &PEMPair{
|
||||||
Cert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}),
|
Cert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}),
|
||||||
Key: pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}),
|
Key: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: marshaledKey}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func marshalECPrivateKey(key *ecdsa.PrivateKey) ([]byte, error) {
|
||||||
|
derBytes, err := x509.MarshalECPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal EC private key: %w", err)
|
||||||
|
}
|
||||||
|
return derBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
func b64Encode(data []byte) string {
|
func b64Encode(data []byte) string {
|
||||||
return base64.StdEncoding.EncodeToString(data)
|
return base64.StdEncoding.EncodeToString(data)
|
||||||
}
|
}
|
||||||
|
@ -63,10 +79,23 @@ func (p *PEMPair) ToTLSCert() (*tls.Certificate, error) {
|
||||||
return &cert, err
|
return &cert, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newSerialNumber() (*big.Int, error) {
|
||||||
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) // 128-bit random number
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate serial number: %w", err)
|
||||||
|
}
|
||||||
|
return serialNumber, nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewAgent() (ca, srv, client *PEMPair, err error) {
|
func NewAgent() (ca, srv, client *PEMPair, err error) {
|
||||||
|
caSerialNumber, err := newSerialNumber()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
// Create the CA's certificate
|
// Create the CA's certificate
|
||||||
caTemplate := &x509.Certificate{
|
caTemplate := &x509.Certificate{
|
||||||
SerialNumber: big.NewInt(1),
|
SerialNumber: caSerialNumber,
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
Organization: []string{"GoDoxy"},
|
Organization: []string{"GoDoxy"},
|
||||||
CommonName: CertsDNSName,
|
CommonName: CertsDNSName,
|
||||||
|
@ -76,9 +105,12 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
||||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||||
BasicConstraintsValid: true,
|
BasicConstraintsValid: true,
|
||||||
IsCA: true,
|
IsCA: true,
|
||||||
|
MaxPathLen: 0,
|
||||||
|
MaxPathLenZero: true,
|
||||||
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||||
}
|
}
|
||||||
|
|
||||||
caKey, err := rsa.GenerateKey(rand.Reader, KeySize)
|
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -91,20 +123,29 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
||||||
ca = toPEMPair(caDER, caKey)
|
ca = toPEMPair(caDER, caKey)
|
||||||
|
|
||||||
// Generate a new private key for the server certificate
|
// Generate a new private key for the server certificate
|
||||||
serverKey, err := rsa.GenerateKey(rand.Reader, KeySize)
|
serverKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serverSerialNumber, err := newSerialNumber()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
srvTemplate := &x509.Certificate{
|
srvTemplate := &x509.Certificate{
|
||||||
SerialNumber: big.NewInt(2),
|
SerialNumber: serverSerialNumber,
|
||||||
Issuer: caTemplate.Subject,
|
Issuer: caTemplate.Subject,
|
||||||
Subject: caTemplate.Subject,
|
Subject: pkix.Name{
|
||||||
|
Organization: caTemplate.Subject.Organization,
|
||||||
|
OrganizationalUnit: []string{"Server"},
|
||||||
|
CommonName: CertsDNSName,
|
||||||
|
},
|
||||||
DNSNames: []string{CertsDNSName},
|
DNSNames: []string{CertsDNSName},
|
||||||
NotBefore: time.Now(),
|
NotBefore: time.Now(),
|
||||||
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
|
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||||
}
|
}
|
||||||
|
|
||||||
srvCertDER, err := x509.CreateCertificate(rand.Reader, srvTemplate, caTemplate, &serverKey.PublicKey, caKey)
|
srvCertDER, err := x509.CreateCertificate(rand.Reader, srvTemplate, caTemplate, &serverKey.PublicKey, caKey)
|
||||||
|
@ -114,20 +155,29 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
||||||
|
|
||||||
srv = toPEMPair(srvCertDER, serverKey)
|
srv = toPEMPair(srvCertDER, serverKey)
|
||||||
|
|
||||||
clientKey, err := rsa.GenerateKey(rand.Reader, KeySize)
|
clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientSerialNumber, err := newSerialNumber()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
clientTemplate := &x509.Certificate{
|
clientTemplate := &x509.Certificate{
|
||||||
SerialNumber: big.NewInt(3),
|
SerialNumber: clientSerialNumber,
|
||||||
Issuer: caTemplate.Subject,
|
Issuer: caTemplate.Subject,
|
||||||
Subject: caTemplate.Subject,
|
Subject: pkix.Name{
|
||||||
|
Organization: caTemplate.Subject.Organization,
|
||||||
|
OrganizationalUnit: []string{"Client"},
|
||||||
|
CommonName: CertsDNSName,
|
||||||
|
},
|
||||||
DNSNames: []string{CertsDNSName},
|
DNSNames: []string{CertsDNSName},
|
||||||
NotBefore: time.Now(),
|
NotBefore: time.Now(),
|
||||||
NotAfter: time.Now().AddDate(1000, 0, 0),
|
NotAfter: time.Now().AddDate(1000, 0, 0),
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||||
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||||
}
|
}
|
||||||
clientCertDER, err := x509.CreateCertificate(rand.Reader, clientTemplate, caTemplate, &clientKey.PublicKey, caKey)
|
clientCertDER, err := x509.CreateCertificate(rand.Reader, clientTemplate, caTemplate, &clientKey.PublicKey, caKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -8,59 +8,59 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewAgent(t *testing.T) {
|
func TestNewAgent(t *testing.T) {
|
||||||
ca, srv, client, err := NewAgent()
|
ca, srv, client, err := NewAgent()
|
||||||
ExpectNoError(t, err)
|
require.NoError(t, err)
|
||||||
ExpectTrue(t, ca != nil)
|
require.NotNil(t, ca)
|
||||||
ExpectTrue(t, srv != nil)
|
require.NotNil(t, srv)
|
||||||
ExpectTrue(t, client != nil)
|
require.NotNil(t, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPEMPair(t *testing.T) {
|
func TestPEMPair(t *testing.T) {
|
||||||
ca, srv, client, err := NewAgent()
|
ca, srv, client, err := NewAgent()
|
||||||
ExpectNoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for i, p := range []*PEMPair{ca, srv, client} {
|
for i, p := range []*PEMPair{ca, srv, client} {
|
||||||
t.Run(fmt.Sprintf("load-%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("load-%d", i), func(t *testing.T) {
|
||||||
var pp PEMPair
|
var pp PEMPair
|
||||||
err := pp.Load(p.String())
|
err := pp.Load(p.String())
|
||||||
ExpectNoError(t, err)
|
require.NoError(t, err)
|
||||||
ExpectEqual(t, p.Cert, pp.Cert)
|
require.Equal(t, p.Cert, pp.Cert)
|
||||||
ExpectEqual(t, p.Key, pp.Key)
|
require.Equal(t, p.Key, pp.Key)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPEMPairToTLSCert(t *testing.T) {
|
func TestPEMPairToTLSCert(t *testing.T) {
|
||||||
ca, srv, client, err := NewAgent()
|
ca, srv, client, err := NewAgent()
|
||||||
ExpectNoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for i, p := range []*PEMPair{ca, srv, client} {
|
for i, p := range []*PEMPair{ca, srv, client} {
|
||||||
t.Run(fmt.Sprintf("toTLSCert-%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("toTLSCert-%d", i), func(t *testing.T) {
|
||||||
cert, err := p.ToTLSCert()
|
cert, err := p.ToTLSCert()
|
||||||
ExpectNoError(t, err)
|
require.NoError(t, err)
|
||||||
ExpectTrue(t, cert != nil)
|
require.NotNil(t, cert)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerClient(t *testing.T) {
|
func TestServerClient(t *testing.T) {
|
||||||
ca, srv, client, err := NewAgent()
|
ca, srv, client, err := NewAgent()
|
||||||
ExpectNoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
srvTLS, err := srv.ToTLSCert()
|
srvTLS, err := srv.ToTLSCert()
|
||||||
ExpectNoError(t, err)
|
require.NoError(t, err)
|
||||||
ExpectTrue(t, srvTLS != nil)
|
require.NotNil(t, srvTLS)
|
||||||
|
|
||||||
clientTLS, err := client.ToTLSCert()
|
clientTLS, err := client.ToTLSCert()
|
||||||
ExpectNoError(t, err)
|
require.NoError(t, err)
|
||||||
ExpectTrue(t, clientTLS != nil)
|
require.NotNil(t, clientTLS)
|
||||||
|
|
||||||
caPool := x509.NewCertPool()
|
caPool := x509.NewCertPool()
|
||||||
ExpectTrue(t, caPool.AppendCertsFromPEM(ca.Cert))
|
require.True(t, caPool.AppendCertsFromPEM(ca.Cert))
|
||||||
|
|
||||||
srvTLSConfig := &tls.Config{
|
srvTLSConfig := &tls.Config{
|
||||||
Certificates: []tls.Certificate{*srvTLS},
|
Certificates: []tls.Certificate{*srvTLS},
|
||||||
|
@ -86,6 +86,6 @@ func TestServerClient(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := httpClient.Get(server.URL)
|
resp, err := httpClient.Get(server.URL)
|
||||||
ExpectNoError(t, err)
|
require.NoError(t, err)
|
||||||
ExpectEqual(t, resp.StatusCode, http.StatusOK)
|
require.Equal(t, resp.StatusCode, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (cfg *AgentConfig) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
func (cfg *AgentConfig) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||||
|
@ -42,8 +42,12 @@ func (cfg *AgentConfig) Fetch(ctx context.Context, endpoint string) ([]byte, int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
func (cfg *AgentConfig) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
||||||
return websocket.Dial(ctx, APIBaseURL+endpoint, &websocket.DialOptions{
|
transport := cfg.Transport()
|
||||||
HTTPClient: cfg.NewHTTPClient(),
|
dialer := websocket.Dialer{
|
||||||
Host: AgentHost,
|
NetDialContext: transport.DialContext,
|
||||||
|
NetDialTLSContext: transport.DialTLSContext,
|
||||||
|
}
|
||||||
|
return dialer.DialContext(ctx, APIBaseURL+endpoint, http.Header{
|
||||||
|
"Host": {AgentHost},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,36 @@ services:
|
||||||
AGENT_PORT: "{{.Port}}"
|
AGENT_PORT: "{{.Port}}"
|
||||||
AGENT_CA_CERT: "{{.CACert}}"
|
AGENT_CA_CERT: "{{.CACert}}"
|
||||||
AGENT_SSL_CERT: "{{.SSLCert}}"
|
AGENT_SSL_CERT: "{{.SSLCert}}"
|
||||||
|
# use agent as a docker socket proxy: [host]:port
|
||||||
|
# set LISTEN_ADDR to enable (e.g. 127.0.0.1:2375)
|
||||||
|
LISTEN_ADDR:
|
||||||
|
POST: false
|
||||||
|
ALLOW_RESTARTS: false
|
||||||
|
ALLOW_START: false
|
||||||
|
ALLOW_STOP: false
|
||||||
|
AUTH: false
|
||||||
|
BUILD: false
|
||||||
|
COMMIT: false
|
||||||
|
CONFIGS: false
|
||||||
|
CONTAINERS: false
|
||||||
|
DISTRIBUTION: false
|
||||||
|
EVENTS: true
|
||||||
|
EXEC: false
|
||||||
|
GRPC: false
|
||||||
|
IMAGES: false
|
||||||
|
INFO: false
|
||||||
|
NETWORKS: false
|
||||||
|
NODES: false
|
||||||
|
PING: true
|
||||||
|
PLUGINS: false
|
||||||
|
SECRETS: false
|
||||||
|
SERVICES: false
|
||||||
|
SESSION: false
|
||||||
|
SWARM: false
|
||||||
|
SYSTEM: false
|
||||||
|
TASKS: false
|
||||||
|
VERSION: true
|
||||||
|
VOLUMES: false
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
16
agent/pkg/env/env.go
vendored
16
agent/pkg/env/env.go
vendored
|
@ -15,10 +15,24 @@ func DefaultAgentName() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
AgentName string
|
||||||
|
AgentPort int
|
||||||
|
AgentSkipClientCertCheck bool
|
||||||
|
AgentCACert string
|
||||||
|
AgentSSLCert string
|
||||||
|
DockerSocket string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() {
|
||||||
|
DockerSocket = common.GetEnvString("DOCKER_SOCKET", "/var/run/docker.sock")
|
||||||
AgentName = common.GetEnvString("AGENT_NAME", DefaultAgentName())
|
AgentName = common.GetEnvString("AGENT_NAME", DefaultAgentName())
|
||||||
AgentPort = common.GetEnvInt("AGENT_PORT", 8890)
|
AgentPort = common.GetEnvInt("AGENT_PORT", 8890)
|
||||||
AgentSkipClientCertCheck = common.GetEnvBool("AGENT_SKIP_CLIENT_CERT_CHECK", false)
|
AgentSkipClientCertCheck = common.GetEnvBool("AGENT_SKIP_CLIENT_CERT_CHECK", false)
|
||||||
|
|
||||||
AgentCACert = common.GetEnvString("AGENT_CA_CERT", "")
|
AgentCACert = common.GetEnvString("AGENT_CA_CERT", "")
|
||||||
AgentSSLCert = common.GetEnvString("AGENT_SSL_CERT", "")
|
AgentSSLCert = common.GetEnvString("AGENT_SSL_CERT", "")
|
||||||
)
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
@ -18,7 +18,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
scheme := query.Get("scheme")
|
scheme := query.Get("scheme")
|
||||||
if scheme == "" {
|
if scheme == "" {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, "missing scheme", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
case "fileserver":
|
case "fileserver":
|
||||||
path := query.Get("path")
|
path := query.Get("path")
|
||||||
if path == "" {
|
if path == "" {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, "missing path", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
|
@ -40,7 +40,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
host := query.Get("host")
|
host := query.Get("host")
|
||||||
path := query.Get("path")
|
path := query.Get("path")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, "missing host", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err = monitor.NewHTTPHealthMonitor(&url.URL{
|
result, err = monitor.NewHTTPHealthMonitor(&url.URL{
|
||||||
|
@ -51,17 +51,18 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
case "tcp", "udp":
|
case "tcp", "udp":
|
||||||
host := query.Get("host")
|
host := query.Get("host")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, "missing host", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hasPort := strings.Contains(host, ":")
|
hasPort := strings.Contains(host, ":")
|
||||||
port := query.Get("port")
|
port := query.Get("port")
|
||||||
if port != "" && !hasPort {
|
if port != "" && hasPort {
|
||||||
host = fmt.Sprintf("%s:%s", host, port)
|
http.Error(w, "port and host with port cannot both be provided", http.StatusBadRequest)
|
||||||
} else {
|
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if port != "" {
|
||||||
|
host = fmt.Sprintf("%s:%s", host, port)
|
||||||
|
}
|
||||||
result, err = monitor.NewRawHealthMonitor(&url.URL{
|
result, err = monitor.NewRawHealthMonitor(&url.URL{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
Host: host,
|
Host: host,
|
||||||
|
@ -73,5 +74,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gphttp.RespondJSON(w, r, result)
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(result)
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,9 +172,9 @@ func TestCheckHealthTCPUDP(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "InvalidHost",
|
name: "InvalidHost",
|
||||||
scheme: "tcp",
|
scheme: "tcp",
|
||||||
host: "invalid",
|
host: "",
|
||||||
port: 8080,
|
port: 8080,
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedHealthy: false,
|
expectedHealthy: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -188,9 +188,17 @@ func TestCheckHealthTCPUDP(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "InvalidHost",
|
name: "InvalidHost",
|
||||||
scheme: "udp",
|
scheme: "udp",
|
||||||
host: "invalid",
|
host: "",
|
||||||
port: 8080,
|
port: 8080,
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
expectedHealthy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Port in both host and port",
|
||||||
|
scheme: "tcp",
|
||||||
|
host: "localhost:1234",
|
||||||
|
port: 1234,
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
expectedHealthy: false,
|
expectedHealthy: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -208,9 +216,11 @@ func TestCheckHealthTCPUDP(t *testing.T) {
|
||||||
|
|
||||||
require.Equal(t, recorder.Code, tt.expectedStatus)
|
require.Equal(t, recorder.Code, tt.expectedStatus)
|
||||||
|
|
||||||
|
if tt.expectedStatus == http.StatusOK {
|
||||||
var result health.HealthCheckResult
|
var result health.HealthCheckResult
|
||||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
||||||
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
func serviceUnavailable(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Error(w, "docker socket is not available", http.StatusServiceUnavailable)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DockerSocketHandler() http.HandlerFunc {
|
|
||||||
dockerClient, err := docker.NewClient(common.DockerHostFromEnv)
|
|
||||||
if err != nil {
|
|
||||||
logging.Warn().Err(err).Msg("failed to connect to docker client")
|
|
||||||
return serviceUnavailable
|
|
||||||
}
|
|
||||||
rp := reverseproxy.NewReverseProxy("docker", types.NewURL(&url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: client.DummyHost,
|
|
||||||
}), dockerClient.HTTPClient().Transport)
|
|
||||||
|
|
||||||
return rp.ServeHTTP
|
|
||||||
}
|
|
|
@ -2,48 +2,35 @@ package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"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/metrics/systeminfo"
|
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/pkg"
|
||||||
|
socketproxy "github.com/yusing/go-proxy/socketproxy/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServeMux struct{ *http.ServeMux }
|
type ServeMux struct{ *http.ServeMux }
|
||||||
|
|
||||||
func (mux ServeMux) HandleMethods(methods, endpoint string, handler http.HandlerFunc) {
|
func (mux ServeMux) HandleEndpoint(method, endpoint string, handler http.HandlerFunc) {
|
||||||
for _, m := range strutils.CommaSeperatedList(methods) {
|
mux.ServeMux.HandleFunc(method+" "+agent.APIEndpointBase+endpoint, handler)
|
||||||
mux.ServeMux.HandleFunc(m+" "+agent.APIEndpointBase+endpoint, handler)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mux ServeMux) HandleFunc(endpoint string, handler http.HandlerFunc) {
|
func (mux ServeMux) HandleFunc(endpoint string, handler http.HandlerFunc) {
|
||||||
mux.ServeMux.HandleFunc(agent.APIEndpointBase+endpoint, handler)
|
mux.ServeMux.HandleFunc(agent.APIEndpointBase+endpoint, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
type NopWriteCloser struct {
|
|
||||||
io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (NopWriteCloser) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAgentHandler() http.Handler {
|
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.HandleEndpoint("GET", agent.EndpointVersion, pkg.GetVersionHTTPHandler())
|
||||||
mux.HandleMethods("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, env.AgentName)
|
fmt.Fprint(w, env.AgentName)
|
||||||
})
|
})
|
||||||
mux.HandleMethods("GET", agent.EndpointHealth, CheckHealth)
|
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
||||||
mux.HandleMethods("GET", agent.EndpointLogs, memlogger.HandlerFunc())
|
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, systeminfo.Poller.ServeHTTP)
|
||||||
mux.HandleMethods("GET", agent.EndpointSystemInfo, systeminfo.Poller.ServeHTTP)
|
mux.ServeMux.HandleFunc("/", socketproxy.DockerSocketHandler(env.DockerSocket))
|
||||||
mux.ServeMux.HandleFunc("/", DockerSocketHandler())
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,26 @@ package handler
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/http/httputil"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agentproxy"
|
"github.com/yusing/go-proxy/agent/pkg/agentproxy"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func NewTransport() *http.Transport {
|
||||||
|
return &http.Transport{
|
||||||
|
MaxIdleConnsPerHost: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
ResponseHeaderTimeout: 60 * time.Second,
|
||||||
|
WriteBufferSize: 16 * 1024, // 16KB
|
||||||
|
ReadBufferSize: 16 * 1024, // 16KB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
|
isHTTPS, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
|
||||||
|
@ -34,11 +42,9 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
scheme = "https"
|
scheme = "https"
|
||||||
}
|
}
|
||||||
|
|
||||||
var transport *http.Transport
|
transport := NewTransport()
|
||||||
if skipTLSVerify {
|
if skipTLSVerify {
|
||||||
transport = gphttp.NewTransportWithTLSConfig(&tls.Config{InsecureSkipVerify: true})
|
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||||
} else {
|
|
||||||
transport = gphttp.NewTransport()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if responseHeaderTimeout > 0 {
|
if responseHeaderTimeout > 0 {
|
||||||
|
@ -49,14 +55,13 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
r.URL.Host = ""
|
r.URL.Host = ""
|
||||||
r.URL.Path = r.URL.Path[agent.HTTPProxyURLPrefixLen:] // strip the {API_BASE}/proxy/http prefix
|
r.URL.Path = r.URL.Path[agent.HTTPProxyURLPrefixLen:] // strip the {API_BASE}/proxy/http prefix
|
||||||
r.RequestURI = r.URL.String()
|
r.RequestURI = r.URL.String()
|
||||||
r.URL.Host = host
|
|
||||||
|
rp := &httputil.ReverseProxy{
|
||||||
|
Director: func(r *http.Request) {
|
||||||
r.URL.Scheme = scheme
|
r.URL.Scheme = scheme
|
||||||
|
r.URL.Host = host
|
||||||
logging.Debug().Msgf("proxy http request: %s %s", r.Method, r.URL.String())
|
},
|
||||||
|
Transport: transport,
|
||||||
rp := reverseproxy.NewReverseProxy("agent", types.NewURL(&url.URL{
|
}
|
||||||
Scheme: scheme,
|
|
||||||
Host: host,
|
|
||||||
}), transport)
|
|
||||||
rp.ServeHTTP(w, r)
|
rp.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/env"
|
"github.com/yusing/go-proxy/agent/pkg/env"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/handler"
|
"github.com/yusing/go-proxy/agent/pkg/handler"
|
||||||
"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/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
)
|
)
|
||||||
|
@ -33,12 +33,11 @@ func StartAgentServer(parent task.Parent, opt Options) {
|
||||||
tlsConfig.ClientAuth = tls.NoClientCert
|
tlsConfig.ClientAuth = tls.NoClientCert
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := logging.GetLogger()
|
|
||||||
agentServer := &http.Server{
|
agentServer := &http.Server{
|
||||||
Addr: fmt.Sprintf(":%d", opt.Port),
|
Addr: fmt.Sprintf(":%d", opt.Port),
|
||||||
Handler: handler.NewAgentHandler(),
|
Handler: handler.NewAgentHandler(),
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Start(parent, agentServer, nil, logger)
|
server.Start(parent, agentServer, nil, &log.Logger)
|
||||||
}
|
}
|
||||||
|
|
105
cmd/main.go
105
cmd/main.go
|
@ -1,29 +1,25 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/query"
|
|
||||||
"github.com/yusing/go-proxy/internal/auth"
|
"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/logging"
|
"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/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"
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rawLogger = log.New(os.Stdout, "", 0)
|
|
||||||
|
|
||||||
func parallel(fns ...func()) {
|
func parallel(fns ...func()) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for _, fn := range fns {
|
for _, fn := range fns {
|
||||||
|
@ -38,103 +34,36 @@ func parallel(fns ...func()) {
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
initProfiling()
|
initProfiling()
|
||||||
args := pkg.GetArgs(common.MainServerCommandValidator{})
|
|
||||||
|
|
||||||
switch args.Command {
|
|
||||||
case common.CommandReload:
|
|
||||||
if err := query.ReloadServer(); err != nil {
|
|
||||||
gperr.LogFatal("server reload error", err)
|
|
||||||
}
|
|
||||||
rawLogger.Println("ok")
|
|
||||||
return
|
|
||||||
case common.CommandListIcons:
|
|
||||||
icons, err := internal.ListAvailableIcons()
|
|
||||||
if err != nil {
|
|
||||||
rawLogger.Fatal(err)
|
|
||||||
}
|
|
||||||
printJSON(icons)
|
|
||||||
return
|
|
||||||
case common.CommandListRoutes:
|
|
||||||
routes, err := query.ListRoutes()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to connect to api server: %s", err)
|
|
||||||
log.Printf("falling back to config file")
|
|
||||||
} else {
|
|
||||||
printJSON(routes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case common.CommandDebugListMTrace:
|
|
||||||
trace, err := query.ListMiddlewareTraces()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
printJSON(trace)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.Command == common.CommandStart {
|
|
||||||
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
||||||
logging.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
|
log.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
|
||||||
logging.Trace().Msg("trace enabled")
|
log.Trace().Msg("trace enabled")
|
||||||
parallel(
|
parallel(
|
||||||
internal.InitIconListCache,
|
dnsproviders.InitProviders,
|
||||||
|
homepage.InitIconListCache,
|
||||||
systeminfo.Poller.Start,
|
systeminfo.Poller.Start,
|
||||||
|
middleware.LoadComposeFiles,
|
||||||
)
|
)
|
||||||
|
|
||||||
if common.APIJWTSecret == nil {
|
if common.APIJWTSecret == nil {
|
||||||
logging.Warn().Msg("API_JWT_SECRET is not set, using random key")
|
log.Warn().Msg("API_JWT_SECRET is not set, using random key")
|
||||||
common.APIJWTSecret = common.RandomJWTKey()
|
common.APIJWTSecret = common.RandomJWTKey()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
logging.DiscardLogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.Command == common.CommandValidate {
|
|
||||||
data, err := os.ReadFile(common.ConfigPath)
|
|
||||||
if err == nil {
|
|
||||||
err = config.Validate(data)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("config error: ", err)
|
|
||||||
}
|
|
||||||
log.Print("config OK")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dir := range common.RequiredDirectories {
|
for _, dir := range common.RequiredDirectories {
|
||||||
prepareDirectory(dir)
|
prepareDirectory(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware.LoadComposeFiles()
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
var cfg *config.Config
|
|
||||||
var err gperr.Error
|
|
||||||
if cfg, err = config.Load(); err != nil {
|
|
||||||
gperr.LogWarn("errors in config", err)
|
gperr.LogWarn("errors in config", err)
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch args.Command {
|
|
||||||
case common.CommandListRoutes:
|
|
||||||
cfg.StartProxyProviders()
|
|
||||||
printJSON(routes.ByAlias())
|
|
||||||
return
|
|
||||||
case common.CommandListConfigs:
|
|
||||||
printJSON(cfg.Value())
|
|
||||||
return
|
|
||||||
case common.CommandDebugListEntries:
|
|
||||||
printJSON(cfg.DumpRoutes())
|
|
||||||
return
|
|
||||||
case common.CommandDebugListProviders:
|
|
||||||
printJSON(cfg.DumpRouteProviders())
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Start(&config.StartServersOptions{
|
cfg.Start(&config.StartServersOptions{
|
||||||
Proxy: true,
|
Proxy: true,
|
||||||
})
|
})
|
||||||
if err := auth.Initialize(); err != nil {
|
if err := auth.Initialize(); err != nil {
|
||||||
logging.Fatal().Err(err).Msg("failed to initialize authentication")
|
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||||
}
|
}
|
||||||
// API Handler needs to start after auth is initialized.
|
// API Handler needs to start after auth is initialized.
|
||||||
cfg.StartServers(&config.StartServersOptions{
|
cfg.StartServers(&config.StartServersOptions{
|
||||||
|
@ -150,15 +79,7 @@ func main() {
|
||||||
func prepareDirectory(dir string) {
|
func prepareDirectory(dir string) {
|
||||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
if err = os.MkdirAll(dir, 0o755); err != nil {
|
if err = os.MkdirAll(dir, 0o755); err != nil {
|
||||||
logging.Fatal().Msgf("failed to create directory %s: %v", dir, err)
|
log.Fatal().Msgf("failed to create directory %s: %v", dir, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printJSON(obj any) {
|
|
||||||
j, err := json.MarshalIndent(obj, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
logging.Fatal().Err(err).Send()
|
|
||||||
}
|
|
||||||
rawLogger.Print(string(j)) // raw output for convenience using "jq"
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,18 +3,43 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const mb = 1024 * 1024
|
||||||
|
|
||||||
func initProfiling() {
|
func initProfiling() {
|
||||||
runtime.GOMAXPROCS(2)
|
debug.SetGCPercent(-1)
|
||||||
debug.SetMemoryLimit(100 * 1024 * 1024)
|
debug.SetMemoryLimit(50 * mb)
|
||||||
debug.SetMaxStack(15 * 1024 * 1024)
|
debug.SetMaxStack(4 * mb)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.Println(http.ListenAndServe(":7777", nil))
|
log.Info().Msgf("pprof server started at http://localhost:7777/debug/pprof/")
|
||||||
|
log.Error().Err(http.ListenAndServe(":7777", nil)).Msg("pprof server failed")
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Second * 10)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
log.Info().Msgf("-----------------------------------------------------")
|
||||||
|
log.Info().Msgf("Timestamp: %s", time.Now().Format(time.RFC3339))
|
||||||
|
log.Info().Msgf(" Go Heap - In Use (Alloc/HeapAlloc): %s", strutils.FormatByteSize(m.Alloc))
|
||||||
|
log.Info().Msgf(" Go Heap - Reserved from OS (HeapSys): %s", strutils.FormatByteSize(m.HeapSys))
|
||||||
|
log.Info().Msgf(" Go Stacks - In Use (StackInuse): %s", strutils.FormatByteSize(m.StackInuse))
|
||||||
|
log.Info().Msgf(" Go Runtime - Other Sys (MSpanInuse, MCacheInuse, BuckHashSys, GCSys, OtherSys): %s", strutils.FormatByteSize(m.MSpanInuse+m.MCacheInuse+m.BuckHashSys+m.GCSys+m.OtherSys))
|
||||||
|
log.Info().Msgf(" Go Runtime - Total from OS (Sys): %s", strutils.FormatByteSize(m.Sys))
|
||||||
|
log.Info().Msgf(" Number of Goroutines: %d", runtime.NumGoroutine())
|
||||||
|
log.Info().Msgf(" Number of GCs: %d", m.NumGC)
|
||||||
|
log.Info().Msg("-----------------------------------------------------")
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,46 @@
|
||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
|
socket-proxy:
|
||||||
|
container_name: socket-proxy
|
||||||
|
image: ghcr.io/yusing/socket-proxy:latest
|
||||||
|
environment:
|
||||||
|
- ALLOW_START=1
|
||||||
|
- ALLOW_STOP=1
|
||||||
|
- ALLOW_RESTARTS=1
|
||||||
|
- CONTAINERS=1
|
||||||
|
- EVENTS=1
|
||||||
|
- INFO=1
|
||||||
|
- PING=1
|
||||||
|
- POST=1
|
||||||
|
- VERSION=1
|
||||||
|
volumes:
|
||||||
|
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
|
||||||
|
restart: unless-stopped
|
||||||
|
tmpfs:
|
||||||
|
- /run
|
||||||
|
ports:
|
||||||
|
- ${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}:2375
|
||||||
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
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
||||||
|
read_only: true
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- all
|
||||||
depends_on:
|
depends_on:
|
||||||
- app
|
- app
|
||||||
environment:
|
environment:
|
||||||
|
HOSTNAME: 127.0.0.1
|
||||||
PORT: ${GODOXY_FRONTEND_PORT:-3000}
|
PORT: ${GODOXY_FRONTEND_PORT:-3000}
|
||||||
|
|
||||||
# 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.#1.port: ${GODOXY_FRONTEND_PORT:-3000}
|
||||||
# proxy.godoxy.middlewares.cidr_whitelist: |
|
# proxy.#1.middlewares.cidr_whitelist: |
|
||||||
# status: 403
|
# status: 403
|
||||||
# message: IP not allowed
|
# message: IP not allowed
|
||||||
# allow:
|
# allow:
|
||||||
|
@ -24,16 +49,27 @@ 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-proxy
|
||||||
restart: always
|
restart: always
|
||||||
network_mode: host # do not change this
|
network_mode: host # do not change this
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
||||||
|
depends_on:
|
||||||
|
socket-proxy:
|
||||||
|
condition: service_started
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- all
|
||||||
|
cap_add:
|
||||||
|
- NET_BIND_SERVICE
|
||||||
|
environment:
|
||||||
|
- DOCKER_HOST=tcp://${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}
|
||||||
volumes:
|
volumes:
|
||||||
- /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:ro
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
|
||||||
# To use autocert, certs will be stored in "./certs".
|
# To use autocert, certs will be stored in "./certs".
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
# options:
|
# options:
|
||||||
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||||
|
|
||||||
# 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://docs.godoxy.dev/DNS-01-Providers
|
||||||
|
|
||||||
# acl:
|
# acl:
|
||||||
# default: allow # or deny (default: allow)
|
# default: allow # or deny (default: allow)
|
||||||
|
@ -38,10 +38,25 @@
|
||||||
|
|
||||||
entrypoint:
|
entrypoint:
|
||||||
# Below define an example of middleware config
|
# Below define an example of middleware config
|
||||||
# 1. block non local IP connections
|
# 1. set security headers
|
||||||
# 2. redirect HTTP to HTTPS
|
# 2. block non local IP connections
|
||||||
|
# 3. redirect HTTP to HTTPS
|
||||||
#
|
#
|
||||||
# middlewares:
|
middlewares:
|
||||||
|
- use: CloudflareRealIP
|
||||||
|
- use: ModifyResponse
|
||||||
|
set_headers:
|
||||||
|
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
|
||||||
|
Access-Control-Allow-Headers: "*"
|
||||||
|
Access-Control-Allow-Origin: "*"
|
||||||
|
Access-Control-Max-Age: 180
|
||||||
|
Vary: "*"
|
||||||
|
X-XSS-Protection: 1; mode=block
|
||||||
|
Content-Security-Policy: "object-src 'self'; frame-ancestors 'self';"
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
X-Frame-Options: SAMEORIGIN
|
||||||
|
Referrer-Policy: same-origin
|
||||||
|
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
|
||||||
# - use: CIDRWhitelist
|
# - use: CIDRWhitelist
|
||||||
# allow:
|
# allow:
|
||||||
# - "127.0.0.1"
|
# - "127.0.0.1"
|
||||||
|
@ -100,7 +115,7 @@ providers:
|
||||||
# secret: aaaa-bbbb-cccc-dddd
|
# secret: aaaa-bbbb-cccc-dddd
|
||||||
# no_tls_verify: true
|
# no_tls_verify: true
|
||||||
|
|
||||||
# Check https://github.com/yusing/godoxy/wiki/Certificates-and-domain-matching#domain-matching
|
# Check https://docs.godoxy.dev/Certificates-and-domain-matching
|
||||||
# for explaination of `match_domains`
|
# for explaination of `match_domains`
|
||||||
#
|
#
|
||||||
# match_domains:
|
# match_domains:
|
||||||
|
|
231
go.mod
231
go.mod
|
@ -1,55 +1,63 @@
|
||||||
module github.com/yusing/go-proxy
|
module github.com/yusing/go-proxy
|
||||||
|
|
||||||
go 1.24.2
|
go 1.24.5
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy/agent => ./agent
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy/internal/dnsproviders => ./internal/dnsproviders
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy/internal/utils => ./internal/utils
|
||||||
|
|
||||||
|
replace github.com/coreos/go-oidc/v3 => github.com/godoxy-app/go-oidc/v3 v3.0.0-20250523122447-f078841dec22
|
||||||
|
|
||||||
|
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250523125835-a2474a6ebe30
|
||||||
|
|
||||||
|
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250607110153-34d627ba1b5d
|
||||||
|
|
||||||
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/coreos/go-oidc/v3 v3.14.1 // oidc authentication
|
github.com/coreos/go-oidc/v3 v3.14.1 // oidc authentication
|
||||||
github.com/docker/docker v28.1.1+incompatible // docker daemon
|
github.com/docker/docker v28.3.2+incompatible // docker daemon
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||||
github.com/go-acme/lego/v4 v4.23.1 // acme client
|
github.com/go-acme/lego/v4 v4.25.1 // acme client
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // validator
|
github.com/go-playground/validator/v10 v10.27.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/gotify/server/v2 v2.6.1 // reference the Message struct for json response
|
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||||
|
github.com/gotify/server/v2 v2.6.3 // 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/puzpuzpuz/xsync/v3 v3.5.1 // lock free map for concurrent operations
|
github.com/puzpuzpuz/xsync/v4 v4.1.0 // 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.6 // system info metrics
|
||||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||||
golang.org/x/crypto v0.37.0 // encrypting password with bcrypt
|
golang.org/x/crypto v0.40.0 // encrypting password with bcrypt
|
||||||
golang.org/x/net v0.39.0 // HTTP header utilities
|
golang.org/x/net v0.42.0 // HTTP header utilities
|
||||||
golang.org/x/oauth2 v0.29.0 // oauth2 authentication
|
golang.org/x/oauth2 v0.30.0 // oauth2 authentication
|
||||||
golang.org/x/text v0.24.0 // string utilities
|
golang.org/x/sync v0.16.0
|
||||||
golang.org/x/time v0.11.0 // time utilities
|
golang.org/x/time v0.12.0 // time utilities
|
||||||
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.2
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.13.2
|
github.com/docker/cli v28.3.2+incompatible
|
||||||
github.com/docker/cli v28.1.1+incompatible
|
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
|
||||||
github.com/goccy/go-yaml v1.17.1
|
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
|
||||||
github.com/luthermonson/go-proxmox v0.2.2
|
github.com/luthermonson/go-proxmox v0.2.2
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1
|
github.com/oschwald/maxminddb-golang v1.13.1
|
||||||
github.com/quic-go/quic-go v0.51.0
|
github.com/quic-go/quic-go v0.54.0
|
||||||
github.com/samber/slog-zerolog/v2 v2.7.3
|
github.com/samber/slog-zerolog/v2 v2.7.3
|
||||||
github.com/spf13/afero v1.14.0
|
github.com/spf13/afero v1.14.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
go.uber.org/atomic v1.11.0
|
github.com/yusing/go-proxy/agent v0.0.0-20250721013917-68ac4f952d1e
|
||||||
|
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-20250721013917-68ac4f952d1e
|
||||||
|
github.com/yusing/go-proxy/internal/utils v0.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250418000134-7af8fd7b079e
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/auth v0.16.1 // indirect
|
cloud.google.com/go/auth v0.16.3 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.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/azcore v1.18.1 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // 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/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/privatedns/armprivatedns v1.3.0 // indirect
|
||||||
|
@ -58,98 +66,84 @@ require (
|
||||||
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/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.106 // indirect
|
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.36.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.29.18 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.71 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // 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/accept-encoding v1.12.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.51.1 // indirect
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 // indirect
|
||||||
github.com/aws/smithy-go v1.22.3 // indirect
|
github.com/aws/smithy-go v1.22.5 // indirect
|
||||||
github.com/baidubce/bce-sdk-go v0.9.224 // indirect
|
github.com/baidubce/bce-sdk-go v0.9.236 // indirect
|
||||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
github.com/boombuler/barcode v1.0.2 // indirect
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
github.com/buger/goterm v1.0.4 // indirect
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // 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/cloudwego/base64x v0.1.5 // 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/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/djherbis/times v1.6.0 // indirect
|
||||||
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
github.com/docker/go-connections v0.5.0
|
||||||
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.4 // indirect
|
||||||
github.com/exoscale/egoscale/v3 v3.1.14 // indirect
|
github.com/exoscale/egoscale/v3 v3.1.24 // indirect
|
||||||
github.com/fatih/structs v1.1.0 // 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-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.1 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.3 // 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/go-resty/resty/v2 v2.16.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.4.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
|
||||||
github.com/gofrs/flock v0.12.1 // indirect
|
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/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||||
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
||||||
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // 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-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.146 // indirect
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.160 // indirect
|
||||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||||
github.com/infobloxopen/infoblox-go-client/v2 v2.9.0 // indirect
|
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
|
||||||
github.com/jinzhu/copier v0.4.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/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
|
||||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||||
github.com/labbsr0x/goh v1.0.1 // 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/linode/linodego v1.53.0 // indirect
|
||||||
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
|
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
|
||||||
github.com/liquidweb/liquidweb-go v1.6.4 // 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/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.67 // indirect
|
||||||
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // 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/auroradns v1.1.0 // indirect
|
||||||
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
|
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
|
||||||
github.com/nrdcg/desec v0.11.0 // indirect
|
github.com/nrdcg/desec v0.11.0 // indirect
|
||||||
|
@ -161,12 +155,9 @@ require (
|
||||||
github.com/nrdcg/nodion v0.1.0 // 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/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/ovh/go-ovh v1.9.0 // 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/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/peterhellberg/link v1.2.0 // indirect
|
github.com/peterhellberg/link v1.2.0 // indirect
|
||||||
|
@ -174,20 +165,19 @@ require (
|
||||||
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/pquerna/otp v1.4.0 // indirect
|
github.com/pquerna/otp v1.5.0 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
|
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/sacloud/api-client-go v0.2.10 // indirect
|
github.com/sacloud/api-client-go v0.3.2 // indirect
|
||||||
github.com/sacloud/go-http v0.1.9 // indirect
|
github.com/sacloud/go-http v0.1.9 // indirect
|
||||||
github.com/sacloud/iaas-api-go v1.14.0 // indirect
|
github.com/sacloud/iaas-api-go v1.16.1 // indirect
|
||||||
github.com/sacloud/packages-go v0.0.11 // indirect
|
github.com/sacloud/packages-go v0.0.11 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
github.com/samber/lo v1.49.1 // indirect
|
github.com/samber/lo v1.51.0 // indirect
|
||||||
github.com/samber/slog-common v0.18.1 // indirect
|
github.com/samber/slog-common v0.19.0 // indirect
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 // indirect
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 // indirect
|
||||||
github.com/selectel/domains-go v1.1.0 // 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/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // 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/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||||
|
@ -195,54 +185,63 @@ require (
|
||||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||||
github.com/sony/gobreaker v1.0.0 // indirect
|
github.com/sony/gobreaker v1.0.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/cast v1.7.1 // indirect
|
github.com/spf13/cast v1.9.2 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.7 // indirect
|
||||||
github.com/spf13/viper v1.20.1 // indirect
|
github.com/spf13/viper v1.20.1 // indirect
|
||||||
github.com/subosito/gotenv v1.6.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/common v1.0.1214 // indirect
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 // indirect
|
|
||||||
github.com/tjfoc/gmsm v1.4.1 // 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/transip/gotransip/v6 v6.26.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 // indirect
|
||||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
|
||||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||||
github.com/volcengine/volc-sdk-golang v1.0.205 // indirect
|
github.com/volcengine/volc-sdk-golang v1.0.216 // indirect
|
||||||
github.com/vultr/govultr/v3 v3.19.1 // indirect
|
github.com/vultr/govultr/v3 v3.21.1 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // 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.mongodb.org/mongo-driver v1.17.4 // 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.62.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
go.uber.org/atomic v1.11.0
|
||||||
go.uber.org/mock v0.5.1 // indirect
|
go.uber.org/mock v0.5.2 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/ratelimit v0.3.1 // indirect
|
go.uber.org/ratelimit v0.3.1 // indirect
|
||||||
golang.org/x/arch v0.16.0 // indirect
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
golang.org/x/mod v0.24.0 // indirect
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
golang.org/x/sync v0.13.0 // indirect
|
golang.org/x/text v0.27.0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/tools v0.35.0 // indirect
|
||||||
golang.org/x/tools v0.32.0 // indirect
|
google.golang.org/api v0.243.0 // indirect
|
||||||
google.golang.org/api v0.230.0 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f // indirect
|
google.golang.org/grpc v1.74.2 // 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
|
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
|
||||||
gopkg.in/ns1/ns1-go.v2 v2.14.2 // indirect
|
gopkg.in/ns1/ns1-go.v2 v2.14.4 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
k8s.io/api v0.33.0 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // 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
|
require (
|
||||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.9 // indirect
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
|
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect
|
||||||
|
github.com/alibabacloud-go/tea v1.3.10 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||||
|
github.com/aliyun/credentials-go v1.4.6 // indirect
|
||||||
|
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||||
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
|
||||||
|
github.com/go-acme/alidns-20150109/v4 v4.5.11 // indirect
|
||||||
|
github.com/go-acme/tencentclouddnspod v1.0.1208 // indirect
|
||||||
|
github.com/namedotcom/go/v4 v4.0.2 // indirect
|
||||||
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.96.0 // indirect
|
||||||
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.96.0 // indirect
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.3 // indirect
|
||||||
|
github.com/onsi/gomega v1.36.3 // indirect
|
||||||
|
github.com/selectel/go-selvpcclient/v4 v4.1.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,16 +2,14 @@ package acl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/oschwald/maxminddb-golang"
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/rs/zerolog"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
acl "github.com/yusing/go-proxy/internal/acl/types"
|
|
||||||
"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/accesslog"
|
"github.com/yusing/go-proxy/internal/logging/accesslog"
|
||||||
|
"github.com/yusing/go-proxy/internal/maxmind"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
@ -19,43 +17,24 @@ import (
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Default string `json:"default" validate:"omitempty,oneof=allow deny"` // default: allow
|
Default string `json:"default" validate:"omitempty,oneof=allow deny"` // default: allow
|
||||||
AllowLocal *bool `json:"allow_local"` // default: true
|
AllowLocal *bool `json:"allow_local"` // default: true
|
||||||
Allow []string `json:"allow"`
|
Allow Matchers `json:"allow"`
|
||||||
Deny []string `json:"deny"`
|
Deny Matchers `json:"deny"`
|
||||||
Log *accesslog.ACLLoggerConfig `json:"log"`
|
Log *accesslog.ACLLoggerConfig `json:"log"`
|
||||||
|
|
||||||
MaxMind *MaxMindConfig `json:"maxmind" validate:"omitempty"`
|
|
||||||
|
|
||||||
config
|
config
|
||||||
|
valErr gperr.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
type config struct {
|
||||||
defaultAllow bool
|
defaultAllow bool
|
||||||
allowLocal bool
|
allowLocal bool
|
||||||
allow []matcher
|
ipCache *xsync.Map[string, *checkCache]
|
||||||
deny []matcher
|
|
||||||
ipCache *xsync.MapOf[string, *checkCache]
|
|
||||||
logAllowed bool
|
logAllowed bool
|
||||||
logger *accesslog.AccessLogger
|
logger *accesslog.AccessLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
type checkCache struct {
|
type checkCache struct {
|
||||||
*acl.IPInfo
|
*maxmind.IPInfo
|
||||||
allow bool
|
allow bool
|
||||||
created time.Time
|
created time.Time
|
||||||
}
|
}
|
||||||
|
@ -63,7 +42,7 @@ type checkCache struct {
|
||||||
const cacheTTL = 1 * time.Minute
|
const cacheTTL = 1 * time.Minute
|
||||||
|
|
||||||
func (c *checkCache) Expired() bool {
|
func (c *checkCache) Expired() bool {
|
||||||
return c.created.Add(cacheTTL).After(utils.TimeNow())
|
return c.created.Add(cacheTTL).Before(utils.TimeNow())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add stats
|
// TODO: add stats
|
||||||
|
@ -73,11 +52,6 @@ const (
|
||||||
ACLDeny = "deny"
|
ACLDeny = "deny"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
MaxMindGeoLite MaxMindDatabaseType = "geolite"
|
|
||||||
MaxMindGeoIP2 MaxMindDatabaseType = "geoip2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Config) Validate() gperr.Error {
|
func (c *Config) Validate() gperr.Error {
|
||||||
switch c.Default {
|
switch c.Default {
|
||||||
case "", ACLAllow:
|
case "", ACLAllow:
|
||||||
|
@ -85,7 +59,8 @@ func (c *Config) Validate() gperr.Error {
|
||||||
case ACLDeny:
|
case ACLDeny:
|
||||||
c.defaultAllow = false
|
c.defaultAllow = false
|
||||||
default:
|
default:
|
||||||
return gperr.New("invalid default value").Subject(c.Default)
|
c.valErr = gperr.New("invalid default value").Subject(c.Default)
|
||||||
|
return c.valErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.AllowLocal != nil {
|
if c.AllowLocal != nil {
|
||||||
|
@ -94,55 +69,24 @@ func (c *Config) Validate() gperr.Error {
|
||||||
c.allowLocal = true
|
c.allowLocal = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.MaxMind != nil {
|
|
||||||
c.MaxMind.logger = logging.With().Str("type", string(c.MaxMind.Database)).Logger()
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Log != nil {
|
if c.Log != nil {
|
||||||
c.logAllowed = c.Log.LogAllowed
|
c.logAllowed = c.Log.LogAllowed
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := gperr.NewBuilder("syntax error")
|
if !c.allowLocal && !c.defaultAllow && len(c.Allow) == 0 {
|
||||||
c.allow = make([]matcher, 0, len(c.Allow))
|
c.valErr = gperr.New("allow_local is false and default is deny, but no allow rules are configured")
|
||||||
c.deny = make([]matcher, 0, len(c.Deny))
|
return c.valErr
|
||||||
|
|
||||||
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.ipCache = xsync.NewMap[string, *checkCache]()
|
||||||
c.allow = nil
|
|
||||||
c.deny = nil
|
|
||||||
return errMatcherFormat.With(errs.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
c.ipCache = xsync.NewMapOf[string, *checkCache]()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Valid() bool {
|
func (c *Config) Valid() bool {
|
||||||
return c != nil && (len(c.allow) > 0 || len(c.deny) > 0 || c.allowLocal)
|
return c != nil && c.valErr == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Start(parent *task.Task) gperr.Error {
|
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 {
|
if c.Log != nil {
|
||||||
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -150,10 +94,22 @@ func (c *Config) Start(parent *task.Task) gperr.Error {
|
||||||
}
|
}
|
||||||
c.logger = logger
|
c.logger = logger
|
||||||
}
|
}
|
||||||
|
if c.valErr != nil {
|
||||||
|
return c.valErr
|
||||||
|
}
|
||||||
|
log.Info().
|
||||||
|
Str("default", c.Default).
|
||||||
|
Bool("allow_local", c.allowLocal).
|
||||||
|
Int("allow_rules", len(c.Allow)).
|
||||||
|
Int("deny_rules", len(c.Deny)).
|
||||||
|
Msg("ACL started")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *config) cacheRecord(info *acl.IPInfo, allow bool) {
|
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
|
||||||
|
if common.ForceResolveCountry && info.City == nil {
|
||||||
|
maxmind.LookupCity(info)
|
||||||
|
}
|
||||||
c.ipCache.Store(info.Str, &checkCache{
|
c.ipCache.Store(info.Str, &checkCache{
|
||||||
IPInfo: info,
|
IPInfo: info,
|
||||||
allow: allow,
|
allow: allow,
|
||||||
|
@ -161,7 +117,7 @@ func (c *config) cacheRecord(info *acl.IPInfo, allow bool) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *config) log(info *acl.IPInfo, allowed bool) {
|
func (c *config) log(info *maxmind.IPInfo, allowed bool) {
|
||||||
if c.logger == nil {
|
if c.logger == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -175,14 +131,13 @@ func (c *Config) IPAllowed(ip net.IP) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// always allow private and loopback
|
// always allow loopback, not logged
|
||||||
// loopback is not logged
|
|
||||||
if ip.IsLoopback() {
|
if ip.IsLoopback() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.allowLocal && ip.IsPrivate() {
|
if c.allowLocal && ip.IsPrivate() {
|
||||||
c.log(&acl.IPInfo{IP: ip, Str: ip.String()}, true)
|
c.log(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,21 +148,17 @@ func (c *Config) IPAllowed(ip net.IP) bool {
|
||||||
return record.allow
|
return record.allow
|
||||||
}
|
}
|
||||||
|
|
||||||
ipAndStr := &acl.IPInfo{IP: ip, Str: ipStr}
|
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
|
||||||
for _, m := range c.allow {
|
if c.Allow.Match(ipAndStr) {
|
||||||
if m(ipAndStr) {
|
|
||||||
c.log(ipAndStr, true)
|
c.log(ipAndStr, true)
|
||||||
c.cacheRecord(ipAndStr, true)
|
c.cacheRecord(ipAndStr, true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
if c.Deny.Match(ipAndStr) {
|
||||||
for _, m := range c.deny {
|
|
||||||
if m(ipAndStr) {
|
|
||||||
c.log(ipAndStr, false)
|
c.log(ipAndStr, false)
|
||||||
c.cacheRecord(ipAndStr, false)
|
c.cacheRecord(ipAndStr, false)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
c.log(ipAndStr, c.defaultAllow)
|
c.log(ipAndStr, c.defaultAllow)
|
||||||
c.cacheRecord(ipAndStr, c.defaultAllow)
|
c.cacheRecord(ipAndStr, c.defaultAllow)
|
||||||
|
|
|
@ -4,11 +4,17 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
acl "github.com/yusing/go-proxy/internal/acl/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/maxmind"
|
||||||
)
|
)
|
||||||
|
|
||||||
type matcher func(*acl.IPInfo) bool
|
type MatcherFunc func(*maxmind.IPInfo) bool
|
||||||
|
|
||||||
|
type Matcher struct {
|
||||||
|
match MatcherFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type Matchers []Matcher
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MatcherTypeIP = "ip"
|
MatcherTypeIP = "ip"
|
||||||
|
@ -17,6 +23,9 @@ const (
|
||||||
MatcherTypeCountry = "country"
|
MatcherTypeCountry = "country"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: use this error in the future
|
||||||
|
//
|
||||||
|
//nolint:unused
|
||||||
var errMatcherFormat = gperr.Multiline().AddLines(
|
var errMatcherFormat = gperr.Multiline().AddLines(
|
||||||
"invalid matcher format, expect {type}:{value}",
|
"invalid matcher format, expect {type}:{value}",
|
||||||
"Available types: ip|cidr|tz|country",
|
"Available types: ip|cidr|tz|country",
|
||||||
|
@ -25,62 +34,66 @@ var errMatcherFormat = gperr.Multiline().AddLines(
|
||||||
"tz:Asia/Shanghai",
|
"tz:Asia/Shanghai",
|
||||||
"country:GB",
|
"country:GB",
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errSyntax = gperr.New("syntax error")
|
errSyntax = gperr.New("syntax error")
|
||||||
errInvalidIP = gperr.New("invalid IP")
|
errInvalidIP = gperr.New("invalid IP")
|
||||||
errInvalidCIDR = gperr.New("invalid CIDR")
|
errInvalidCIDR = gperr.New("invalid CIDR")
|
||||||
errMaxMindNotConfigured = gperr.New("MaxMind not configured")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (cfg *Config) parseMatcher(s string) (matcher, gperr.Error) {
|
func (matcher *Matcher) Parse(s string) error {
|
||||||
parts := strings.Split(s, ":")
|
parts := strings.Split(s, ":")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return nil, errSyntax
|
return errSyntax
|
||||||
}
|
}
|
||||||
|
|
||||||
switch parts[0] {
|
switch parts[0] {
|
||||||
case MatcherTypeIP:
|
case MatcherTypeIP:
|
||||||
ip := net.ParseIP(parts[1])
|
ip := net.ParseIP(parts[1])
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
return nil, errInvalidIP
|
return errInvalidIP
|
||||||
}
|
}
|
||||||
return matchIP(ip), nil
|
matcher.match = matchIP(ip)
|
||||||
case MatcherTypeCIDR:
|
case MatcherTypeCIDR:
|
||||||
_, net, err := net.ParseCIDR(parts[1])
|
_, net, err := net.ParseCIDR(parts[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errInvalidCIDR
|
return errInvalidCIDR
|
||||||
}
|
}
|
||||||
return matchCIDR(net), nil
|
matcher.match = matchCIDR(net)
|
||||||
case MatcherTypeTimeZone:
|
case MatcherTypeTimeZone:
|
||||||
if cfg.MaxMind == nil {
|
matcher.match = matchTimeZone(parts[1])
|
||||||
return nil, errMaxMindNotConfigured
|
|
||||||
}
|
|
||||||
return cfg.MaxMind.matchTimeZone(parts[1]), nil
|
|
||||||
case MatcherTypeCountry:
|
case MatcherTypeCountry:
|
||||||
if cfg.MaxMind == nil {
|
matcher.match = matchISOCode(parts[1])
|
||||||
return nil, errMaxMindNotConfigured
|
|
||||||
}
|
|
||||||
return cfg.MaxMind.matchISOCode(parts[1]), nil
|
|
||||||
default:
|
default:
|
||||||
return nil, errSyntax
|
return errSyntax
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchIP(ip net.IP) matcher {
|
func (matchers Matchers) Match(ip *maxmind.IPInfo) bool {
|
||||||
return func(ip2 *acl.IPInfo) bool {
|
for _, m := range matchers {
|
||||||
|
if m.match(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchIP(ip net.IP) MatcherFunc {
|
||||||
|
return func(ip2 *maxmind.IPInfo) bool {
|
||||||
return ip.Equal(ip2.IP)
|
return ip.Equal(ip2.IP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchCIDR(n *net.IPNet) matcher {
|
func matchCIDR(n *net.IPNet) MatcherFunc {
|
||||||
return func(ip *acl.IPInfo) bool {
|
return func(ip *maxmind.IPInfo) bool {
|
||||||
return n.Contains(ip.IP)
|
return n.Contains(ip.IP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *MaxMindConfig) matchTimeZone(tz string) matcher {
|
func matchTimeZone(tz string) MatcherFunc {
|
||||||
return func(ip *acl.IPInfo) bool {
|
return func(ip *maxmind.IPInfo) bool {
|
||||||
city, ok := cfg.lookupCity(ip)
|
city, ok := maxmind.LookupCity(ip)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -88,9 +101,9 @@ func (cfg *MaxMindConfig) matchTimeZone(tz string) matcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *MaxMindConfig) matchISOCode(iso string) matcher {
|
func matchISOCode(iso string) MatcherFunc {
|
||||||
return func(ip *acl.IPInfo) bool {
|
return func(ip *maxmind.IPInfo) bool {
|
||||||
city, ok := cfg.lookupCity(ip)
|
city, ok := maxmind.LookupCity(ip)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
49
internal/acl/matcher_test.go
Normal file
49
internal/acl/matcher_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
maxmind "github.com/yusing/go-proxy/internal/maxmind/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/serialization"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMatchers(t *testing.T) {
|
||||||
|
strMatchers := []string{
|
||||||
|
"ip:127.0.0.1",
|
||||||
|
"cidr:10.0.0.0/8",
|
||||||
|
}
|
||||||
|
|
||||||
|
var mathers Matchers
|
||||||
|
err := serialization.Convert(reflect.ValueOf(strMatchers), reflect.ValueOf(&mathers), false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
ip string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"127.0.0.1", true},
|
||||||
|
{"10.0.0.1", true},
|
||||||
|
{"127.0.0.2", false},
|
||||||
|
{"192.168.0.1", false},
|
||||||
|
{"11.0.0.1", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
ip := net.ParseIP(test.ip)
|
||||||
|
if ip == nil {
|
||||||
|
t.Fatalf("invalid ip: %s", test.ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := mathers.Match(&maxmind.IPInfo{
|
||||||
|
IP: ip,
|
||||||
|
Str: test.ip,
|
||||||
|
})
|
||||||
|
if got != test.want {
|
||||||
|
t.Errorf("mathers.Match(%s) = %v, want %v", test.ip, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,281 +0,0 @@
|
||||||
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)
|
|
||||||
exists := 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exists = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
cfg.logger.Info().Msg("MaxMind DB not found/invalid, downloading...")
|
|
||||||
reader, err = cfg.download()
|
|
||||||
if err != nil {
|
|
||||||
return ErrDownloadFailure.With(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
reader, err := cfg.download()
|
|
||||||
if err != nil {
|
|
||||||
cfg.logger.Err(err).Msg("failed to update MaxMind DB")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cfg.db.Lock()
|
|
||||||
cfg.db.Close()
|
|
||||||
cfg.db.Reader = reader
|
|
||||||
cfg.setLastUpdate(*remoteLastModified)
|
|
||||||
cfg.db.Unlock()
|
|
||||||
|
|
||||||
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() (*maxminddb.Reader, error) {
|
|
||||||
resp, err := newReq(cfg, http.MethodGet)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
path := dbPath(cfg.Database)
|
|
||||||
tmpPath := path + "-tmp.tar.gz"
|
|
||||||
file, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.logger.Info().Msg("MaxMind DB downloading...")
|
|
||||||
|
|
||||||
_, err = io.Copy(file, resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
file.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
file.Close()
|
|
||||||
|
|
||||||
// extract .tar.gz and move only the dbFilename to path
|
|
||||||
err = extractFileFromTarGz(tmpPath, dbFilename(cfg.Database), path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gperr.New("failed to extract database from archive").With(err)
|
|
||||||
}
|
|
||||||
// cleanup the tar.gz file
|
|
||||||
_ = os.Remove(tmpPath)
|
|
||||||
|
|
||||||
db, err := maxmindDBOpen(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractFileFromTarGz(tarGzPath, targetFilename, destPath string) error {
|
|
||||||
f, err := os.Open(tarGzPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
gzr, err := gzip.NewReader(f)
|
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,213 +0,0 @@
|
||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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) {
|
|
||||||
io.Copy(w, strings.NewReader("FAKEMMDB"))
|
|
||||||
}))
|
|
||||||
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 }()
|
|
||||||
|
|
||||||
rw := &fakeReadCloser{}
|
|
||||||
oldNewReq := newReq
|
|
||||||
newReq = func(cfg *MaxMindConfig, method string) (*http.Response, error) {
|
|
||||||
return &http.Response{
|
|
||||||
StatusCode: http.StatusOK,
|
|
||||||
Body: rw,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
defer func() { newReq = oldNewReq }()
|
|
||||||
|
|
||||||
db, err := cfg.download()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("download() error = %v", err)
|
|
||||||
}
|
|
||||||
if db == nil {
|
|
||||||
t.Error("expected db instance")
|
|
||||||
}
|
|
||||||
if !rw.closed {
|
|
||||||
t.Error("expected rw to be closed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,9 @@
|
||||||
package acl
|
package acl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TCPListener struct {
|
type TCPListener struct {
|
||||||
|
@ -9,12 +11,23 @@ type TCPListener struct {
|
||||||
lis net.Listener
|
lis net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) WrapTCP(lis net.Listener) net.Listener {
|
type noConn struct{}
|
||||||
if cfg == nil {
|
|
||||||
|
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 (c *Config) WrapTCP(lis net.Listener) net.Listener {
|
||||||
|
if c == nil {
|
||||||
return lis
|
return lis
|
||||||
}
|
}
|
||||||
return &TCPListener{
|
return &TCPListener{
|
||||||
acl: cfg,
|
acl: c,
|
||||||
lis: lis,
|
lis: lis,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,11 +45,11 @@ func (s *TCPListener) Accept() (net.Conn, error) {
|
||||||
if !ok {
|
if !ok {
|
||||||
// Not a TCPAddr, drop
|
// Not a TCPAddr, drop
|
||||||
c.Close()
|
c.Close()
|
||||||
return nil, nil
|
return noConn{}, nil
|
||||||
}
|
}
|
||||||
if !s.acl.IPAllowed(addr.IP) {
|
if !s.acl.IPAllowed(addr.IP) {
|
||||||
c.Close()
|
c.Close()
|
||||||
return nil, nil
|
return noConn{}, nil
|
||||||
}
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,12 @@ type UDPListener struct {
|
||||||
lis net.PacketConn
|
lis net.PacketConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) WrapUDP(lis net.PacketConn) net.PacketConn {
|
func (c *Config) WrapUDP(lis net.PacketConn) net.PacketConn {
|
||||||
if cfg == nil {
|
if c == nil {
|
||||||
return lis
|
return lis
|
||||||
}
|
}
|
||||||
return &UDPListener{
|
return &UDPListener{
|
||||||
acl: cfg,
|
acl: c,
|
||||||
lis: lis,
|
lis: lis,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,10 @@ import (
|
||||||
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/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/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/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
"github.com/yusing/go-proxy/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -44,7 +46,7 @@ func (mux ServeMux) HandleFunc(methods, endpoint string, h any, requireAuth ...b
|
||||||
origHandler := handler
|
origHandler := handler
|
||||||
handler = func(w http.ResponseWriter, r *http.Request) {
|
handler = func(w http.ResponseWriter, r *http.Request) {
|
||||||
if httpheaders.IsWebsocket(r.Header) {
|
if httpheaders.IsWebsocket(r.Header) {
|
||||||
httpheaders.SetWebsocketAllowedDomains(r.Header, matchDomains)
|
gpwebsocket.SetWebsocketAllowedDomains(r.Header, matchDomains)
|
||||||
}
|
}
|
||||||
origHandler(w, r)
|
origHandler(w, r)
|
||||||
}
|
}
|
||||||
|
@ -65,13 +67,19 @@ 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)
|
||||||
mux.HandleFunc("GET", "/v1/list", v1.List, true)
|
mux.HandleFunc("GET", "/v1/list", v1.ListRoutesHandler, true)
|
||||||
mux.HandleFunc("GET", "/v1/list/{what}", v1.List, true)
|
mux.HandleFunc("GET", "/v1/list/routes", v1.ListRoutesHandler, true)
|
||||||
mux.HandleFunc("GET", "/v1/list/{what}/{which}", v1.List, true)
|
mux.HandleFunc("GET", "/v1/list/route/{which}", v1.ListRouteHandler, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/list/routes_by_provider", v1.ListRoutesByProviderHandler, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/list/files", v1.ListFilesHandler, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/list/homepage_config", v1.ListHomepageConfigHandler, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/list/route_providers", v1.ListRouteProvidersHandler, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/list/homepage_categories", v1.ListHomepageCategoriesHandler, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/list/icons", v1.ListIconsHandler, true)
|
||||||
mux.HandleFunc("GET", "/v1/file/{type}/{filename}", v1.GetFileContent, true)
|
mux.HandleFunc("GET", "/v1/file/{type}/{filename}", v1.GetFileContent, true)
|
||||||
mux.HandleFunc("POST,PUT", "/v1/file/{type}/{filename}", v1.SetFileContent, true)
|
mux.HandleFunc("POST,PUT", "/v1/file/{type}/{filename}", v1.SetFileContent, true)
|
||||||
mux.HandleFunc("POST", "/v1/file/validate/{type}", v1.ValidateFile, true)
|
mux.HandleFunc("POST", "/v1/file/validate/{type}", v1.ValidateFile, true)
|
||||||
|
@ -96,8 +104,8 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
mux.HandleFunc("GET", "/v1/auth/check", auth.AuthCheckHandler)
|
mux.HandleFunc("GET", "/v1/auth/check", auth.AuthCheckHandler)
|
||||||
mux.HandleFunc("GET", "/v1/auth/redirect", defaultAuth.LoginHandler)
|
mux.HandleFunc("GET,POST", "/v1/auth/redirect", defaultAuth.LoginHandler)
|
||||||
mux.HandleFunc("GET", "/v1/auth/callback", defaultAuth.PostAuthCallbackHandler)
|
mux.HandleFunc("GET,POST", "/v1/auth/callback", defaultAuth.PostAuthCallbackHandler)
|
||||||
mux.HandleFunc("GET,POST", "/v1/auth/logout", defaultAuth.LogoutHandler)
|
mux.HandleFunc("GET,POST", "/v1/auth/logout", defaultAuth.LogoutHandler)
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ package certapi
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
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"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"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/net/gphttp/gpwebsocket"
|
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||||
)
|
)
|
||||||
|
@ -19,11 +19,9 @@ func RenewCert(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
conn, err := gpwebsocket.Initiate(w, r)
|
conn, err := gpwebsocket.Initiate(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//nolint:errcheck
|
defer conn.Close()
|
||||||
defer conn.CloseNow()
|
|
||||||
|
|
||||||
logs, cancel := memlogger.Events()
|
logs, cancel := memlogger.Events()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -35,9 +33,9 @@ func RenewCert(w http.ResponseWriter, r *http.Request) {
|
||||||
err = autocert.ObtainCert()
|
err = autocert.ObtainCert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gperr.LogError("failed to obtain cert", err)
|
gperr.LogError("failed to obtain cert", err)
|
||||||
gpwebsocket.WriteText(r, conn, err.Error())
|
_ = gpwebsocket.WriteText(conn, err.Error())
|
||||||
} else {
|
} else {
|
||||||
logging.Info().Msg("cert obtained successfully")
|
log.Info().Msg("cert obtained successfully")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
for {
|
for {
|
||||||
|
@ -46,7 +44,7 @@ func RenewCert(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !gpwebsocket.WriteText(r, conn, string(l)) {
|
if err := gpwebsocket.WriteText(conn, string(l)); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case <-done:
|
case <-done:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -51,12 +52,12 @@ func (t FileType) GetPath(filename string) string {
|
||||||
func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
|
func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
|
||||||
fileType = FileType(r.PathValue("type"))
|
fileType = FileType(r.PathValue("type"))
|
||||||
if !fileType.IsValid() {
|
if !fileType.IsValid() {
|
||||||
err = gphttp.ErrInvalidKey("type")
|
err = fmt.Errorf("invalid file type: %s", fileType)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
filename = r.PathValue("filename")
|
filename = r.PathValue("filename")
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
err = gphttp.ErrMissingKey("filename")
|
err = fmt.Errorf("missing filename")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
package dockerapi
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
const reqTimeout = 10 * time.Second
|
|
|
@ -18,7 +18,7 @@ type Container struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Containers(w http.ResponseWriter, r *http.Request) {
|
func Containers(w http.ResponseWriter, r *http.Request) {
|
||||||
serveHTTP[Container, []Container](w, r, GetContainers)
|
serveHTTP[Container](w, r, GetContainers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {
|
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
package dockerapi
|
package dockerapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/pkg/stdcopy"
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"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/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FIXME: agent logs not updating.
|
||||||
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")
|
||||||
|
@ -22,7 +26,7 @@ func Logs(w http.ResponseWriter, r *http.Request) {
|
||||||
until := query.Get("to")
|
until := query.Get("to")
|
||||||
levels := query.Get("levels") // TODO: implement levels
|
levels := query.Get("levels") // TODO: implement levels
|
||||||
|
|
||||||
dockerClient, found, err := getDockerClient(w, server)
|
dockerClient, found, err := getDockerClient(server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gphttp.BadRequest(w, err.Error())
|
gphttp.BadRequest(w, err.Error())
|
||||||
return
|
return
|
||||||
|
@ -31,6 +35,7 @@ func Logs(w http.ResponseWriter, r *http.Request) {
|
||||||
gphttp.NotFound(w, "server not found")
|
gphttp.NotFound(w, "server not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer dockerClient.Close()
|
||||||
|
|
||||||
opts := container.LogsOptions{
|
opts := container.LogsOptions{
|
||||||
ShowStdout: stdout,
|
ShowStdout: stdout,
|
||||||
|
@ -56,12 +61,15 @@ func Logs(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.CloseNow()
|
defer conn.Close()
|
||||||
|
|
||||||
writer := gpwebsocket.NewWriter(r.Context(), conn, websocket.MessageText)
|
writer := gpwebsocket.NewWriter(r.Context(), conn, websocket.TextMessage)
|
||||||
_, err = stdcopy.StdCopy(writer, writer, logs) // de-multiplex logs
|
_, err = stdcopy.StdCopy(writer, writer, logs) // de-multiplex logs
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Err(err).
|
if errors.Is(err, context.Canceled) || errors.Is(err, task.ErrProgramExiting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Err(err).
|
||||||
Str("server", server).
|
Str("server", server).
|
||||||
Str("container", containerID).
|
Str("container", containerID).
|
||||||
Msg("failed to de-multiplex logs")
|
Msg("failed to de-multiplex logs")
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/coder/websocket/wsjson"
|
"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/docker"
|
"github.com/yusing/go-proxy/internal/docker"
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
@ -44,7 +44,7 @@ func getDockerClients() (DockerClients, gperr.Error) {
|
||||||
dockerClients[name] = dockerClient
|
dockerClients[name] = dockerClient
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, agent := range cfg.ListAgents() {
|
for _, agent := range agent.ListAgents() {
|
||||||
dockerClient, err := docker.NewClient(agent.FakeDockerHost())
|
dockerClient, err := docker.NewClient(agent.FakeDockerHost())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
connErrs.Add(err)
|
connErrs.Add(err)
|
||||||
|
@ -56,7 +56,7 @@ func getDockerClients() (DockerClients, gperr.Error) {
|
||||||
return dockerClients, connErrs.Error()
|
return dockerClients, connErrs.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDockerClient(w http.ResponseWriter, server string) (*docker.SharedClient, bool, error) {
|
func getDockerClient(server string) (*docker.SharedClient, bool, error) {
|
||||||
cfg := config.GetInstance()
|
cfg := config.GetInstance()
|
||||||
var host string
|
var host string
|
||||||
for name, h := range cfg.Value().Providers.Docker {
|
for name, h := range cfg.Value().Providers.Docker {
|
||||||
|
@ -65,12 +65,14 @@ func getDockerClient(w http.ResponseWriter, server string) (*docker.SharedClient
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, agent := range cfg.ListAgents() {
|
if host == "" {
|
||||||
|
for _, agent := range agent.ListAgents() {
|
||||||
if agent.Name() == server {
|
if agent.Name() == server {
|
||||||
host = agent.FakeDockerHost()
|
host = agent.FakeDockerHost()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if host == "" {
|
if host == "" {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
|
@ -98,7 +100,7 @@ func handleResult[V any, T ResultType[V]](w http.ResponseWriter, errs error, res
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(result)
|
json.NewEncoder(w).Encode(result) //nolint
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveHTTP[V any, T ResultType[V]](w http.ResponseWriter, r *http.Request, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
|
func serveHTTP[V any, T ResultType[V]](w http.ResponseWriter, r *http.Request, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
|
||||||
|
@ -115,10 +117,10 @@ func serveHTTP[V any, T ResultType[V]](w http.ResponseWriter, r *http.Request, g
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return wsjson.Write(r.Context(), conn, result)
|
return conn.WriteJSON(result)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
result, err := getResult(r.Context(), dockerClients)
|
result, err := getResult(r.Context(), dockerClients)
|
||||||
handleResult[V, T](w, err, result)
|
handleResult[V](w, err, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
package favicon
|
package favicon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"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/net/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"
|
||||||
|
@ -21,11 +19,11 @@ import (
|
||||||
func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||||
url, alias := req.FormValue("url"), req.FormValue("alias")
|
url, alias := req.FormValue("url"), req.FormValue("alias")
|
||||||
if url == "" && alias == "" {
|
if url == "" && alias == "" {
|
||||||
gphttp.ClientError(w, gphttp.ErrMissingKey("url or alias"), http.StatusBadRequest)
|
gphttp.MissingKey(w, "url or alias")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if url != "" && alias != "" {
|
if url != "" && alias != "" {
|
||||||
gphttp.ClientError(w, gperr.New("url and alias are mutually exclusive"), http.StatusBadRequest)
|
gphttp.BadRequest(w, "url and alias are mutually exclusive")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +31,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||||
if url != "" {
|
if url != "" {
|
||||||
var iconURL homepage.IconURL
|
var iconURL homepage.IconURL
|
||||||
if err := iconURL.Parse(url); err != nil {
|
if err := iconURL.Parse(url); err != nil {
|
||||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
gphttp.ClientError(w, req, err, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fetchResult := homepage.FetchFavIconFromURL(req.Context(), &iconURL)
|
fetchResult := homepage.FetchFavIconFromURL(req.Context(), &iconURL)
|
||||||
|
@ -46,10 +44,16 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// try with alias
|
||||||
|
GetFavIconFromAlias(w, req, alias)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFavIconFromAlias(w http.ResponseWriter, req *http.Request, alias string) {
|
||||||
// try with route.Icon
|
// try with route.Icon
|
||||||
r, ok := routes.HTTP.Get(alias)
|
r, ok := routes.HTTP.Get(alias)
|
||||||
if !ok {
|
if !ok {
|
||||||
gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound)
|
gphttp.ValueNotFound(w, "route", alias)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +61,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||||
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 = homepage.FindIcon(req.Context(), r, hp.Icon.Value)
|
result = homepage.FindIcon(req.Context(), r, *hp.Icon.FullURL)
|
||||||
} else {
|
} else {
|
||||||
result = homepage.FetchFavIconFromURL(req.Context(), hp.Icon)
|
result = homepage.FetchFavIconFromURL(req.Context(), hp.Icon)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/coder/websocket/wsjson"
|
|
||||||
"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"
|
||||||
|
@ -15,7 +14,7 @@ import (
|
||||||
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, routes.HealthMap())
|
return conn.WriteJSON(routes.HealthMap())
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
gphttp.RespondJSON(w, r, routes.HealthMap())
|
gphttp.RespondJSON(w, r, routes.HealthMap())
|
||||||
|
|
|
@ -43,7 +43,7 @@ func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
data, err := io.ReadAll(r.Body)
|
data, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
gphttp.ClientError(w, r, err, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
r.Body.Close()
|
r.Body.Close()
|
||||||
|
@ -53,21 +53,21 @@ func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
|
||||||
case HomepageOverrideItem:
|
case HomepageOverrideItem:
|
||||||
var params HomepageOverrideItemParams
|
var params HomepageOverrideItemParams
|
||||||
if err := json.Unmarshal(data, ¶ms); err != nil {
|
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
gphttp.ClientError(w, r, err, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
overrides.OverrideItem(params.Which, ¶ms.Value)
|
overrides.OverrideItem(params.Which, ¶ms.Value)
|
||||||
case HomepageOverrideItemsBatch:
|
case HomepageOverrideItemsBatch:
|
||||||
var params HomepageOverrideItemsBatchParams
|
var params HomepageOverrideItemsBatchParams
|
||||||
if err := json.Unmarshal(data, ¶ms); err != nil {
|
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
gphttp.ClientError(w, r, err, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
overrides.OverrideItems(params.Value)
|
overrides.OverrideItems(params.Value)
|
||||||
case HomepageOverrideItemVisible: // POST /v1/item_visible [a,b,c], false => hide a, b, c
|
case HomepageOverrideItemVisible: // POST /v1/item_visible [a,b,c], false => hide a, b, c
|
||||||
var params HomepageOverrideItemVisibleParams
|
var params HomepageOverrideItemVisibleParams
|
||||||
if err := json.Unmarshal(data, ¶ms); err != nil {
|
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
gphttp.ClientError(w, r, err, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if params.Value {
|
if params.Value {
|
||||||
|
@ -78,7 +78,7 @@ func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
|
||||||
case HomepageOverrideCategoryOrder:
|
case HomepageOverrideCategoryOrder:
|
||||||
var params HomepageOverrideCategoryOrderParams
|
var params HomepageOverrideCategoryOrderParams
|
||||||
if err := json.Unmarshal(data, ¶ms); err != nil {
|
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
gphttp.ClientError(w, r, err, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
overrides.SetCategoryOrder(params.Which, params.Value)
|
overrides.SetCategoryOrder(params.Which, params.Value)
|
||||||
|
|
|
@ -1,128 +0,0 @@
|
||||||
package v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
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/middleware"
|
|
||||||
"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/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ListRoute = "route"
|
|
||||||
ListRoutes = "routes"
|
|
||||||
ListFiles = "files"
|
|
||||||
ListMiddlewares = "middlewares"
|
|
||||||
ListMiddlewareTraces = "middleware_trace"
|
|
||||||
ListMatchDomains = "match_domains"
|
|
||||||
ListHomepageConfig = "homepage_config"
|
|
||||||
ListRouteProviders = "route_providers"
|
|
||||||
ListHomepageCategories = "homepage_categories"
|
|
||||||
ListIcons = "icons"
|
|
||||||
ListTasks = "tasks"
|
|
||||||
)
|
|
||||||
|
|
||||||
func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
|
||||||
what := r.PathValue("what")
|
|
||||||
if what == "" {
|
|
||||||
what = ListRoutes
|
|
||||||
}
|
|
||||||
which := r.PathValue("which")
|
|
||||||
|
|
||||||
switch what {
|
|
||||||
case ListRoute:
|
|
||||||
route := listRoute(which)
|
|
||||||
if route == nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
} else {
|
|
||||||
gphttp.RespondJSON(w, r, route)
|
|
||||||
}
|
|
||||||
case ListRoutes:
|
|
||||||
gphttp.RespondJSON(w, r, routes.ByAlias(route.RouteType(r.FormValue("type"))))
|
|
||||||
case ListFiles:
|
|
||||||
listFiles(w, r)
|
|
||||||
case ListMiddlewares:
|
|
||||||
gphttp.RespondJSON(w, r, middleware.All())
|
|
||||||
case ListMiddlewareTraces:
|
|
||||||
gphttp.RespondJSON(w, r, middleware.GetAllTrace())
|
|
||||||
case ListMatchDomains:
|
|
||||||
gphttp.RespondJSON(w, r, cfg.Value().MatchDomains)
|
|
||||||
case ListHomepageConfig:
|
|
||||||
gphttp.RespondJSON(w, r, routes.HomepageConfig(r.FormValue("category"), r.FormValue("provider")))
|
|
||||||
case ListRouteProviders:
|
|
||||||
gphttp.RespondJSON(w, r, cfg.RouteProviderList())
|
|
||||||
case ListHomepageCategories:
|
|
||||||
gphttp.RespondJSON(w, r, routes.HomepageCategories())
|
|
||||||
case ListIcons:
|
|
||||||
limit, err := strconv.Atoi(r.FormValue("limit"))
|
|
||||||
if err != nil {
|
|
||||||
limit = 0
|
|
||||||
}
|
|
||||||
icons, err := internal.SearchIcons(r.FormValue("keyword"), limit)
|
|
||||||
if err != nil {
|
|
||||||
gphttp.ClientError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if icons == nil {
|
|
||||||
icons = []string{}
|
|
||||||
}
|
|
||||||
gphttp.RespondJSON(w, r, icons)
|
|
||||||
case ListTasks:
|
|
||||||
gphttp.RespondJSON(w, r, task.DebugTaskList())
|
|
||||||
default:
|
|
||||||
gphttp.BadRequest(w, fmt.Sprintf("invalid what: %s", what))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if which is "all" or empty, return map[string]Route of all routes
|
|
||||||
// otherwise, return a single Route with alias which or nil if not found.
|
|
||||||
func listRoute(which string) any {
|
|
||||||
if which == "" || which == "all" {
|
|
||||||
return routes.ByAlias()
|
|
||||||
}
|
|
||||||
routes := routes.ByAlias()
|
|
||||||
route, ok := routes[which]
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return route
|
|
||||||
}
|
|
||||||
|
|
||||||
func listFiles(w http.ResponseWriter, r *http.Request) {
|
|
||||||
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
|
|
||||||
if err != nil {
|
|
||||||
gphttp.ServerError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp := map[FileType][]string{
|
|
||||||
FileTypeConfig: make([]string, 0),
|
|
||||||
FileTypeProvider: make([]string, 0),
|
|
||||||
FileTypeMiddleware: make([]string, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
t := fileType(file)
|
|
||||||
file = strings.TrimPrefix(file, common.ConfigBasePath+"/")
|
|
||||||
resp[t] = append(resp[t], file)
|
|
||||||
}
|
|
||||||
|
|
||||||
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
|
|
||||||
if err != nil {
|
|
||||||
gphttp.ServerError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, mid := range mids {
|
|
||||||
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
|
|
||||||
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
|
|
||||||
}
|
|
||||||
gphttp.RespondJSON(w, r, resp)
|
|
||||||
}
|
|
|
@ -4,21 +4,19 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/coder/websocket/wsjson"
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
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/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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListAgents(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
func ListAgents(w http.ResponseWriter, r *http.Request) {
|
||||||
if httpheaders.IsWebsocket(r.Header) {
|
if httpheaders.IsWebsocket(r.Header) {
|
||||||
gpwebsocket.Periodic(w, r, 10*time.Second, func(conn *websocket.Conn) error {
|
gpwebsocket.Periodic(w, r, 10*time.Second, func(conn *websocket.Conn) error {
|
||||||
wsjson.Write(r.Context(), conn, cfg.ListAgents())
|
return conn.WriteJSON(agent.ListAgents())
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
gphttp.RespondJSON(w, r, cfg.ListAgents())
|
gphttp.RespondJSON(w, r, agent.ListAgents())
|
||||||
}
|
}
|
||||||
}
|
}
|
41
internal/api/v1/list_files.go
Normal file
41
internal/api/v1/list_files.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListFilesHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp := map[FileType][]string{
|
||||||
|
FileTypeConfig: make([]string, 0),
|
||||||
|
FileTypeProvider: make([]string, 0),
|
||||||
|
FileTypeMiddleware: make([]string, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
t := fileType(file)
|
||||||
|
file = strings.TrimPrefix(file, common.ConfigBasePath+"/")
|
||||||
|
resp[t] = append(resp[t], file)
|
||||||
|
}
|
||||||
|
|
||||||
|
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, mid := range mids {
|
||||||
|
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
|
||||||
|
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
|
||||||
|
}
|
||||||
|
gphttp.RespondJSON(w, r, resp)
|
||||||
|
}
|
13
internal/api/v1/list_homepage_categories.go
Normal file
13
internal/api/v1/list_homepage_categories.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListHomepageCategoriesHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
gphttp.RespondJSON(w, r, routes.HomepageCategories())
|
||||||
|
}
|
13
internal/api/v1/list_homepage_config.go
Normal file
13
internal/api/v1/list_homepage_config.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListHomepageConfigHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
gphttp.RespondJSON(w, r, routes.HomepageConfig(r.FormValue("category"), r.FormValue("provider")))
|
||||||
|
}
|
23
internal/api/v1/list_icons.go
Normal file
23
internal/api/v1/list_icons.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListIconsHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
limit, err := strconv.Atoi(r.FormValue("limit"))
|
||||||
|
if err != nil {
|
||||||
|
limit = 0
|
||||||
|
}
|
||||||
|
icons, err := homepage.SearchIcons(r.FormValue("keyword"), limit)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ClientError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gphttp.RespondJSON(w, r, icons)
|
||||||
|
}
|
19
internal/api/v1/list_route.go
Normal file
19
internal/api/v1/list_route.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListRouteHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
which := r.PathValue("which")
|
||||||
|
route, ok := routes.Get(which)
|
||||||
|
if ok {
|
||||||
|
gphttp.RespondJSON(w, r, route)
|
||||||
|
} else {
|
||||||
|
gphttp.RespondJSON(w, r, nil)
|
||||||
|
}
|
||||||
|
}
|
22
internal/api/v1/list_route_providers.go
Normal file
22
internal/api/v1/list_route_providers.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
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/gpwebsocket"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListRouteProvidersHandler(cfgInstance config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if httpheaders.IsWebsocket(r.Header) {
|
||||||
|
gpwebsocket.Periodic(w, r, 5*time.Second, func(conn *websocket.Conn) error {
|
||||||
|
return conn.WriteJSON(cfgInstance.RouteProviderList())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
gphttp.RespondJSON(w, r, cfgInstance.RouteProviderList())
|
||||||
|
}
|
||||||
|
}
|
25
internal/api/v1/list_routes.go
Normal file
25
internal/api/v1/list_routes.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListRoutesHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
rts := make([]routes.Route, 0)
|
||||||
|
provider := r.FormValue("provider")
|
||||||
|
if provider == "" {
|
||||||
|
gphttp.RespondJSON(w, r, slices.Collect(routes.Iter))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for r := range routes.Iter {
|
||||||
|
if r.ProviderName() == provider {
|
||||||
|
rts = append(rts, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gphttp.RespondJSON(w, r, rts)
|
||||||
|
}
|
13
internal/api/v1/list_routes_by_provider.go
Normal file
13
internal/api/v1/list_routes_by_provider.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListRoutesByProviderHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
gphttp.RespondJSON(w, r, routes.ByProvider())
|
||||||
|
}
|
|
@ -20,27 +20,27 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
name := q.Get("name")
|
name := q.Get("name")
|
||||||
if name == "" {
|
if name == "" {
|
||||||
gphttp.ClientError(w, gphttp.ErrMissingKey("name"))
|
gphttp.MissingKey(w, "name")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
host := q.Get("host")
|
host := q.Get("host")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
gphttp.ClientError(w, gphttp.ErrMissingKey("host"))
|
gphttp.MissingKey(w, "host")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
portStr := q.Get("port")
|
portStr := q.Get("port")
|
||||||
if portStr == "" {
|
if portStr == "" {
|
||||||
gphttp.ClientError(w, gphttp.ErrMissingKey("port"))
|
gphttp.MissingKey(w, "port")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
port, err := strconv.Atoi(portStr)
|
port, err := strconv.Atoi(portStr)
|
||||||
if err != nil || port < 1 || port > 65535 {
|
if err != nil || port < 1 || port > 65535 {
|
||||||
gphttp.ClientError(w, gphttp.ErrInvalidKey("port"))
|
gphttp.InvalidKey(w, "port")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hostport := fmt.Sprintf("%s:%d", host, port)
|
hostport := fmt.Sprintf("%s:%d", host, port)
|
||||||
if _, ok := config.GetInstance().GetAgent(hostport); ok {
|
if _, ok := agent.GetAgent(hostport); ok {
|
||||||
gphttp.ClientError(w, gphttp.ErrAlreadyExists("agent", hostport), http.StatusConflict)
|
gphttp.KeyAlreadyExists(w, "agent", hostport)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t := q.Get("type")
|
t := q.Get("type")
|
||||||
|
@ -48,10 +48,10 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
case "docker", "system":
|
case "docker", "system":
|
||||||
break
|
break
|
||||||
case "":
|
case "":
|
||||||
gphttp.ClientError(w, gphttp.ErrMissingKey("type"))
|
gphttp.MissingKey(w, "type")
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
gphttp.ClientError(w, gphttp.ErrInvalidKey("type"))
|
gphttp.InvalidKey(w, "type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,13 +109,13 @@ func VerifyNewAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(clientPEMData, &data); err != nil {
|
if err := json.Unmarshal(clientPEMData, &data); err != nil {
|
||||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
gphttp.ClientError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(data.Host, data.CA, data.Client)
|
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(data.Host, data.CA, data.Client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gphttp.ClientError(w, err)
|
gphttp.ClientError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ func VerifyNewAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
filename, ok := certs.AgentCertsFilepath(data.Host)
|
filename, ok := certs.AgentCertsFilepath(data.Host)
|
||||||
if !ok {
|
if !ok {
|
||||||
gphttp.ClientError(w, gphttp.ErrInvalidKey("host"))
|
gphttp.InvalidKey(w, "host")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
package query
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ReloadServer() gperr.Error {
|
|
||||||
resp, err := gphttp.Post(common.APIHTTPURL+"/v1/reload", "", nil)
|
|
||||||
if err != nil {
|
|
||||||
return gperr.Wrap(err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
failure := gperr.Errorf("server reload status %v", resp.StatusCode)
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return failure.With(err)
|
|
||||||
}
|
|
||||||
reloadErr := string(body)
|
|
||||||
return failure.Withf(reloadErr)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func List[T any](what string) (_ T, outErr gperr.Error) {
|
|
||||||
resp, err := gphttp.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, what))
|
|
||||||
if err != nil {
|
|
||||||
outErr = gperr.Wrap(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
outErr = gperr.Errorf("list %s: failed, status %v", what, resp.StatusCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var res T
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&res)
|
|
||||||
if err != nil {
|
|
||||||
outErr = gperr.Wrap(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListRoutes() (map[string]map[string]any, gperr.Error) {
|
|
||||||
return List[map[string]map[string]any](v1.ListRoutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListMiddlewareTraces() (middleware.Traces, gperr.Error) {
|
|
||||||
return List[middleware.Traces](v1.ListMiddlewareTraces)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DebugListTasks() (map[string]any, gperr.Error) {
|
|
||||||
return List[map[string]any](v1.ListTasks)
|
|
||||||
}
|
|
|
@ -4,8 +4,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/coder/websocket/wsjson"
|
|
||||||
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/net/gphttp/gpwebsocket"
|
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||||
|
@ -16,7 +15,7 @@ import (
|
||||||
func Stats(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
func Stats(cfg config.ConfigInstance, 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, getStats(cfg))
|
return conn.WriteJSON(getStats(cfg))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
gphttp.RespondJSON(w, r, getStats(cfg))
|
gphttp.RespondJSON(w, r, getStats(cfg))
|
||||||
|
|
|
@ -4,15 +4,15 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
agentPkg "github.com/yusing/go-proxy/agent/pkg/agent"
|
agentPkg "github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
||||||
"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"
|
||||||
|
nettypes "github.com/yusing/go-proxy/internal/net/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
func SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
agentAddr := query.Get("agent_addr")
|
agentAddr := query.Get("agent_addr")
|
||||||
query.Del("agent_addr")
|
query.Del("agent_addr")
|
||||||
|
@ -21,7 +21,7 @@ func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
agent, ok := cfg.GetAgent(agentAddr)
|
agent, ok := agentPkg.GetAgent(agentAddr)
|
||||||
if !ok {
|
if !ok {
|
||||||
gphttp.NotFound(w, "agent_addr")
|
gphttp.NotFound(w, "agent_addr")
|
||||||
return
|
return
|
||||||
|
@ -40,7 +40,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", nettypes.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()))
|
|
||||||
}
|
|
|
@ -38,22 +38,35 @@ 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 next
|
||||||
|
}
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := defaultAuth.CheckToken(r); err != nil {
|
if err := defaultAuth.CheckToken(r); err != nil {
|
||||||
gphttp.ClientError(w, err, http.StatusUnauthorized)
|
gphttp.Unauthorized(w, err.Error())
|
||||||
} else {
|
return
|
||||||
|
}
|
||||||
next(w, r)
|
next(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
func ProceedNext(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := defaultAuth.CheckToken(r); err != nil {
|
next, ok := r.Context().Value(nextHandlerContextKey).(http.HandlerFunc)
|
||||||
http.Redirect(w, r, "/v1/auth/login", http.StatusFound)
|
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 {
|
} else {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/jsonstore"
|
"github.com/yusing/go-proxy/internal/jsonstore"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,6 +21,10 @@ type oauthRefreshToken struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
Expiry time.Time `json:"expiry"`
|
Expiry time.Time `json:"expiry"`
|
||||||
|
|
||||||
|
result *RefreshResult
|
||||||
|
err error
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
|
@ -27,6 +33,12 @@ type Session struct {
|
||||||
Groups []string `json:"groups"`
|
Groups []string `json:"groups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RefreshResult struct {
|
||||||
|
newSession Session
|
||||||
|
jwt string
|
||||||
|
jwtExpiry time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type sessionClaims struct {
|
type sessionClaims struct {
|
||||||
Session
|
Session
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
|
@ -34,11 +46,11 @@ type sessionClaims struct {
|
||||||
|
|
||||||
type sessionID string
|
type sessionID string
|
||||||
|
|
||||||
var oauthRefreshTokens jsonstore.MapStore[oauthRefreshToken]
|
var oauthRefreshTokens jsonstore.MapStore[*oauthRefreshToken]
|
||||||
|
|
||||||
var (
|
var (
|
||||||
defaultRefreshTokenExpiry = 30 * 24 * time.Hour // 1 month
|
defaultRefreshTokenExpiry = 30 * 24 * time.Hour // 1 month
|
||||||
refreshBefore = 30 * time.Second
|
sessionInvalidateDelay = 3 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -50,7 +62,7 @@ const sessionTokenIssuer = "GoDoxy"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if IsOIDCEnabled() {
|
if IsOIDCEnabled() {
|
||||||
oauthRefreshTokens = jsonstore.Store[oauthRefreshToken]("oauth_refresh_tokens")
|
oauthRefreshTokens = jsonstore.Store[*oauthRefreshToken]("oauth_refresh_tokens")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +73,7 @@ func (token *oauthRefreshToken) expired() bool {
|
||||||
func newSessionID() sessionID {
|
func newSessionID() sessionID {
|
||||||
b := make([]byte, 32)
|
b := make([]byte, 32)
|
||||||
_, _ = rand.Read(b)
|
_, _ = rand.Read(b)
|
||||||
return sessionID(base64.StdEncoding.EncodeToString(b))
|
return sessionID(hex.EncodeToString(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSession(username string, groups []string) Session {
|
func newSession(username string, groups []string) Session {
|
||||||
|
@ -72,35 +84,35 @@ func newSession(username string, groups []string) Session {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getOnceOAuthRefreshToken returns the refresh token for the given session.
|
// getOAuthRefreshToken returns the refresh token for the given session.
|
||||||
//
|
func getOAuthRefreshToken(claims *Session) (*oauthRefreshToken, bool) {
|
||||||
// The token is removed from the store after retrieval.
|
|
||||||
func getOnceOAuthRefreshToken(claims *Session) (*oauthRefreshToken, bool) {
|
|
||||||
token, ok := oauthRefreshTokens.Load(string(claims.SessionID))
|
token, ok := oauthRefreshTokens.Load(string(claims.SessionID))
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
invalidateOAuthRefreshToken(claims.SessionID)
|
|
||||||
if token.expired() {
|
if token.expired() {
|
||||||
|
invalidateOAuthRefreshToken(claims.SessionID)
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.Username != token.Username {
|
if claims.Username != token.Username {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return &token, true
|
return token, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func storeOAuthRefreshToken(sessionID sessionID, username, token string) {
|
func storeOAuthRefreshToken(sessionID sessionID, username, token string) {
|
||||||
oauthRefreshTokens.Store(string(sessionID), oauthRefreshToken{
|
oauthRefreshTokens.Store(string(sessionID), &oauthRefreshToken{
|
||||||
Username: username,
|
Username: username,
|
||||||
RefreshToken: token,
|
RefreshToken: token,
|
||||||
Expiry: time.Now().Add(defaultRefreshTokenExpiry),
|
Expiry: time.Now().Add(defaultRefreshTokenExpiry),
|
||||||
})
|
})
|
||||||
logging.Debug().Str("username", username).Msg("stored oauth refresh token")
|
log.Debug().Str("username", username).Msg("stored oauth refresh token")
|
||||||
}
|
}
|
||||||
|
|
||||||
func invalidateOAuthRefreshToken(sessionID sessionID) {
|
func invalidateOAuthRefreshToken(sessionID sessionID) {
|
||||||
logging.Debug().Str("session_id", string(sessionID)).Msg("invalidating oauth refresh token")
|
log.Debug().Str("session_id", string(sessionID)).Msg("invalidating oauth refresh token")
|
||||||
oauthRefreshTokens.Delete(string(sessionID))
|
oauthRefreshTokens.Delete(string(sessionID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,10 +127,10 @@ func (auth *OIDCProvider) setSessionTokenCookie(w http.ResponseWriter, r *http.R
|
||||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
||||||
signed, err := jwtToken.SignedString(common.APIJWTSecret)
|
signed, err := jwtToken.SignedString(common.APIJWTSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Err(err).Msg("failed to sign session token")
|
log.Err(err).Msg("failed to sign session token")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setTokenCookie(w, r, CookieOauthSessionToken, signed, common.APIJWTTokenTTL)
|
SetTokenCookie(w, r, CookieOauthSessionToken, signed, common.APIJWTTokenTTL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionClaims, valid bool, err error) {
|
func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionClaims, valid bool, err error) {
|
||||||
|
@ -135,51 +147,75 @@ func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionCla
|
||||||
return claims, sessionToken.Valid && claims.Issuer == sessionTokenIssuer, nil
|
return claims, sessionToken.Valid && claims.Issuer == sessionTokenIssuer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *OIDCProvider) TryRefreshToken(w http.ResponseWriter, r *http.Request, sessionJWT string) error {
|
func (auth *OIDCProvider) TryRefreshToken(ctx context.Context, sessionJWT string) (*RefreshResult, error) {
|
||||||
// verify the session cookie
|
// verify the session cookie
|
||||||
claims, valid, err := auth.parseSessionJWT(sessionJWT)
|
claims, valid, err := auth.parseSessionJWT(sessionJWT)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrInvalidSessionToken, err)
|
return nil, fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrInvalidSessionToken, err)
|
||||||
}
|
}
|
||||||
if !valid {
|
if !valid {
|
||||||
return ErrInvalidSessionToken
|
return nil, ErrInvalidSessionToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if refresh is possible
|
// check if refresh is possible
|
||||||
refreshToken, ok := getOnceOAuthRefreshToken(&claims.Session)
|
refreshToken, ok := getOAuthRefreshToken(&claims.Session)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errNoRefreshToken
|
return nil, errNoRefreshToken
|
||||||
}
|
}
|
||||||
|
|
||||||
if !auth.checkAllowed(claims.Username, claims.Groups) {
|
if !auth.checkAllowed(claims.Username, claims.Groups) {
|
||||||
return ErrUserNotAllowed
|
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
|
// this step refreshes the token
|
||||||
// see https://cs.opensource.google/go/x/oauth2/+/refs/tags/v0.29.0:oauth2.go;l=313
|
// see https://cs.opensource.google/go/x/oauth2/+/refs/tags/v0.29.0:oauth2.go;l=313
|
||||||
newToken, err := auth.oauthConfig.TokenSource(r.Context(), &oauth2.Token{
|
newToken, err := auth.oauthConfig.TokenSource(ctx, &oauth2.Token{
|
||||||
RefreshToken: refreshToken.RefreshToken,
|
RefreshToken: refreshToken.RefreshToken,
|
||||||
}).Token()
|
}).Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrRefreshTokenFailure, err)
|
refreshToken.err = fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrRefreshTokenFailure, err)
|
||||||
|
return nil, refreshToken.err
|
||||||
}
|
}
|
||||||
|
|
||||||
idTokenJWT, idToken, err := auth.getIdToken(r.Context(), newToken)
|
idTokenJWT, idToken, err := auth.getIDToken(ctx, newToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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()
|
sessionID := newSessionID()
|
||||||
|
|
||||||
logging.Debug().Str("username", claims.Username).Time("expiry", newToken.Expiry).Msg("refreshed token")
|
log.Debug().Str("username", claims.Username).Time("expiry", newToken.Expiry).Msg("refreshed token")
|
||||||
storeOAuthRefreshToken(sessionID, claims.Username, newToken.RefreshToken)
|
storeOAuthRefreshToken(sessionID, claims.Username, newToken.RefreshToken)
|
||||||
|
|
||||||
// set new idToken and new sessionToken
|
refreshToken.result = &RefreshResult{
|
||||||
auth.setIDTokenCookie(w, r, idTokenJWT, time.Until(idToken.Expiry))
|
newSession: Session{
|
||||||
auth.setSessionTokenCookie(w, r, Session{
|
|
||||||
SessionID: sessionID,
|
SessionID: sessionID,
|
||||||
Username: claims.Username,
|
Username: claims.Username,
|
||||||
Groups: claims.Groups,
|
Groups: claims.Groups,
|
||||||
})
|
},
|
||||||
return nil
|
jwt: idTokenJWT,
|
||||||
|
jwtExpiry: idToken.Expiry,
|
||||||
|
}
|
||||||
|
return refreshToken.result, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,13 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"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/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -38,9 +39,8 @@ type (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CookieOauthState = "godoxy_oidc_state"
|
CookieOauthState = "godoxy_oidc_state"
|
||||||
CookieOauthSessionID = "godoxy_session_id"
|
CookieOauthToken = "godoxy_oauth_token" //nolint:gosec
|
||||||
CookieOauthToken = "godoxy_oauth_token"
|
CookieOauthSessionToken = "godoxy_session_token" //nolint:gosec
|
||||||
CookieOauthSessionToken = "godoxy_session_token"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -49,7 +49,12 @@ const (
|
||||||
OIDCLogoutPath = "/auth/logout"
|
OIDCLogoutPath = "/auth/logout"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errMissingIDToken = errors.New("missing id_token field from oauth token")
|
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.
|
// generateState generates a random string for OIDC state.
|
||||||
const oidcStateLength = 32
|
const oidcStateLength = 32
|
||||||
|
@ -62,9 +67,12 @@ func generateState() string {
|
||||||
|
|
||||||
func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, allowedGroups []string) (*OIDCProvider, error) {
|
func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, allowedGroups []string) (*OIDCProvider, error) {
|
||||||
if len(allowedUsers)+len(allowedGroups) == 0 {
|
if len(allowedUsers)+len(allowedGroups) == 0 {
|
||||||
return nil, errors.New("OIDC users, groups, or both must not be empty")
|
return nil, errors.New("oidc.allowed_users or oidc.allowed_groups are both empty")
|
||||||
}
|
}
|
||||||
provider, err := oidc.NewProvider(context.Background(), issuerURL)
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
provider, err := oidc.NewProvider(ctx, issuerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err)
|
return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -72,7 +80,7 @@ func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, all
|
||||||
endSessionURL, err := url.Parse(provider.EndSessionEndpoint())
|
endSessionURL, err := url.Parse(provider.EndSessionEndpoint())
|
||||||
if err != nil && provider.EndSessionEndpoint() != "" {
|
if err != nil && provider.EndSessionEndpoint() != "" {
|
||||||
// non critical, just warn
|
// non critical, just warn
|
||||||
logging.Warn().
|
log.Warn().
|
||||||
Str("issuer", issuerURL).
|
Str("issuer", issuerURL).
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("failed to parse end session URL")
|
Msg("failed to parse end session URL")
|
||||||
|
@ -84,7 +92,7 @@ func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, all
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: clientSecret,
|
||||||
RedirectURL: "",
|
RedirectURL: "",
|
||||||
Endpoint: provider.Endpoint(),
|
Endpoint: provider.Endpoint(),
|
||||||
Scopes: strutils.CommaSeperatedList(common.OIDCScopes),
|
Scopes: common.OIDCScopes,
|
||||||
},
|
},
|
||||||
oidcProvider: provider,
|
oidcProvider: provider,
|
||||||
oidcVerifier: provider.Verifier(&oidc.Config{
|
oidcVerifier: provider.Verifier(&oidc.Config{
|
||||||
|
@ -122,7 +130,7 @@ func optRedirectPostAuth(r *http.Request) oauth2.AuthCodeOption {
|
||||||
return oauth2.SetAuthURLParam("redirect_uri", "https://"+requestHost(r)+OIDCPostAuthPath)
|
return oauth2.SetAuthURLParam("redirect_uri", "https://"+requestHost(r)+OIDCPostAuthPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *OIDCProvider) getIdToken(ctx context.Context, oauthToken *oauth2.Token) (string, *oidc.IDToken, error) {
|
func (auth *OIDCProvider) getIDToken(ctx context.Context, oauthToken *oauth2.Token) (string, *oidc.IDToken, error) {
|
||||||
idTokenJWT, ok := oauthToken.Extra("id_token").(string)
|
idTokenJWT, ok := oauthToken.Extra("id_token").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", nil, errMissingIDToken
|
return "", nil, errMissingIDToken
|
||||||
|
@ -135,6 +143,14 @@ func (auth *OIDCProvider) getIdToken(ctx context.Context, oauthToken *oauth2.Tok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
|
func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "" {
|
||||||
|
r.URL.Path = OIDCAuthInitPath
|
||||||
|
}
|
||||||
|
if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" {
|
||||||
|
r.URL.Scheme = "https"
|
||||||
|
http.Redirect(w, r, r.URL.String(), http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case OIDCAuthInitPath:
|
case OIDCAuthInitPath:
|
||||||
auth.LoginHandler(w, r)
|
auth.LoginHandler(w, r)
|
||||||
|
@ -147,23 +163,43 @@ func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rateLimit = rate.NewLimiter(rate.Every(time.Second), 1)
|
||||||
|
|
||||||
func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// check for session token
|
// check for session token
|
||||||
sessionToken, err := r.Cookie(CookieOauthSessionToken)
|
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 {
|
if err == nil {
|
||||||
err = auth.TryRefreshToken(w, r, sessionToken.Value)
|
auth.setIDTokenCookie(w, r, result.jwt, time.Until(result.jwtExpiry))
|
||||||
if err != nil {
|
auth.setSessionTokenCookie(w, r, result.newSession)
|
||||||
logging.Debug().Err(err).Msg("failed to refresh token")
|
ProceedNext(w, r)
|
||||||
auth.clearCookie(w, r)
|
return
|
||||||
}
|
}
|
||||||
|
// clear cookies then redirect to home
|
||||||
|
log.Err(err).Msg("failed to refresh token")
|
||||||
|
auth.clearCookie(w, r)
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !rateLimit.Allow() {
|
||||||
|
http.Error(w, "auth rate limit exceeded", http.StatusTooManyRequests)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
state := generateState()
|
state := generateState()
|
||||||
setTokenCookie(w, r, CookieOauthState, state, 300*time.Second)
|
SetTokenCookie(w, r, CookieOauthState, state, 300*time.Second)
|
||||||
// redirect user to Idp
|
// redirect user to Idp
|
||||||
http.Redirect(w, r, auth.oauthConfig.AuthCodeURL(state, optRedirectPostAuth(r)), http.StatusFound)
|
url := auth.oauthConfig.AuthCodeURL(state, optRedirectPostAuth(r))
|
||||||
|
if IsFrontend(r) {
|
||||||
|
w.Header().Set("X-Redirect-To", url)
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, url, http.StatusFound)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseClaims(idToken *oidc.IDToken) (*IDTokenClaims, error) {
|
func parseClaims(idToken *oidc.IDToken) (*IDTokenClaims, error) {
|
||||||
|
@ -172,18 +208,19 @@ func parseClaims(idToken *oidc.IDToken) (*IDTokenClaims, error) {
|
||||||
return nil, fmt.Errorf("failed to parse claims: %w", err)
|
return nil, fmt.Errorf("failed to parse claims: %w", err)
|
||||||
}
|
}
|
||||||
if claim.Username == "" {
|
if claim.Username == "" {
|
||||||
return nil, fmt.Errorf("missing username in ID token")
|
return nil, errors.New("missing username in ID token")
|
||||||
}
|
}
|
||||||
return &claim, nil
|
return &claim, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *OIDCProvider) checkAllowed(user string, groups []string) bool {
|
func (auth *OIDCProvider) checkAllowed(user string, groups []string) bool {
|
||||||
userAllowed := slices.Contains(auth.allowedUsers, user)
|
userAllowed := slices.Contains(auth.allowedUsers, user)
|
||||||
if !userAllowed {
|
if userAllowed {
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
if len(auth.allowedGroups) == 0 {
|
if len(auth.allowedGroups) == 0 {
|
||||||
return true
|
// user is not allowed, but no groups are allowed
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return len(utils.Intersect(groups, auth.allowedGroups)) > 0
|
return len(utils.Intersect(groups, auth.allowedGroups)) > 0
|
||||||
}
|
}
|
||||||
|
@ -235,7 +272,7 @@ func (auth *OIDCProvider) PostAuthCallbackHandler(w http.ResponseWriter, r *http
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
idTokenJWT, idToken, err := auth.getIdToken(r.Context(), oauth2Token)
|
idTokenJWT, idToken, err := auth.getIDToken(r.Context(), oauth2Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gphttp.ServerError(w, r, err)
|
gphttp.ServerError(w, r, err)
|
||||||
return
|
return
|
||||||
|
@ -286,12 +323,12 @@ func (auth *OIDCProvider) LogoutHandler(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *OIDCProvider) setIDTokenCookie(w http.ResponseWriter, r *http.Request, jwt string, ttl time.Duration) {
|
func (auth *OIDCProvider) setIDTokenCookie(w http.ResponseWriter, r *http.Request, jwt string, ttl time.Duration) {
|
||||||
setTokenCookie(w, r, CookieOauthToken, jwt, ttl)
|
SetTokenCookie(w, r, CookieOauthToken, jwt, ttl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *OIDCProvider) clearCookie(w http.ResponseWriter, r *http.Request) {
|
func (auth *OIDCProvider) clearCookie(w http.ResponseWriter, r *http.Request) {
|
||||||
clearTokenCookie(w, r, CookieOauthToken)
|
ClearTokenCookie(w, r, CookieOauthToken)
|
||||||
clearTokenCookie(w, r, CookieOauthSessionToken)
|
ClearTokenCookie(w, r, CookieOauthSessionToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleTestCallback handles OIDC callback in test environment.
|
// handleTestCallback handles OIDC callback in test environment.
|
||||||
|
@ -308,7 +345,7 @@ func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Requ
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create test JWT token
|
// Create test JWT token
|
||||||
setTokenCookie(w, r, CookieOauthToken, "test", time.Hour)
|
SetTokenCookie(w, r, CookieOauthToken, "test", time.Hour)
|
||||||
|
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
@ -24,7 +23,7 @@ import (
|
||||||
func setupMockOIDC(t *testing.T) {
|
func setupMockOIDC(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
provider := (&oidc.ProviderConfig{}).NewProvider(context.TODO())
|
provider := (&oidc.ProviderConfig{}).NewProvider(t.Context())
|
||||||
defaultAuth = &OIDCProvider{
|
defaultAuth = &OIDCProvider{
|
||||||
oauthConfig: &oauth2.Config{
|
oauthConfig: &oauth2.Config{
|
||||||
ClientID: "test-client",
|
ClientID: "test-client",
|
||||||
|
@ -104,7 +103,7 @@ func setupProvider(t *testing.T) *provider {
|
||||||
t.Cleanup(ts.Close)
|
t.Cleanup(ts.Close)
|
||||||
|
|
||||||
// Create a test OIDCProvider.
|
// Create a test OIDCProvider.
|
||||||
providerCtx := oidc.ClientContext(context.Background(), ts.Client())
|
providerCtx := oidc.ClientContext(t.Context(), ts.Client())
|
||||||
keySet := oidc.NewRemoteKeySet(providerCtx, ts.URL+"/.well-known/jwks.json")
|
keySet := oidc.NewRemoteKeySet(providerCtx, ts.URL+"/.well-known/jwks.json")
|
||||||
|
|
||||||
return &provider{
|
return &provider{
|
||||||
|
|
|
@ -100,7 +100,7 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *UserPassAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
func (auth *UserPassAuth) PostAuthCallbackHandler(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"`
|
||||||
|
@ -119,16 +119,17 @@ func (auth *UserPassAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
gphttp.ServerError(w, r, err)
|
gphttp.ServerError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL)
|
SetTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
func (auth *UserPassAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
w.Header().Set("X-Redirect-To", "/login")
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
clearTokenCookie(w, r, auth.TokenCookieName())
|
ClearTokenCookie(w, r, auth.TokenCookieName())
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.LoginHandler(w, req)
|
auth.PostAuthCallbackHandler(w, req)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
ExpectEqual(t, w.Code, http.StatusUnauthorized)
|
ExpectEqual(t, w.Code, http.StatusUnauthorized)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -10,22 +11,29 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrMissingOAuthToken = gperr.New("missing oauth token")
|
|
||||||
ErrMissingSessionToken = gperr.New("missing session token")
|
ErrMissingSessionToken = gperr.New("missing session token")
|
||||||
ErrInvalidOAuthToken = gperr.New("invalid oauth token")
|
|
||||||
ErrInvalidSessionToken = gperr.New("invalid session token")
|
ErrInvalidSessionToken = gperr.New("invalid session token")
|
||||||
ErrUserNotAllowed = gperr.New("user not allowed")
|
ErrUserNotAllowed = gperr.New("user not allowed")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func IsFrontend(r *http.Request) bool {
|
||||||
|
return requestRemoteIP(r) == "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestRemoteIP(r *http.Request) string {
|
||||||
|
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
func requestHost(r *http.Request) string {
|
func requestHost(r *http.Request) string {
|
||||||
// check if it's from backend
|
// check if it's from backend
|
||||||
switch r.Host {
|
if IsFrontend(r) {
|
||||||
case common.APIHTTPAddr:
|
|
||||||
// use XFH
|
|
||||||
return r.Header.Get("X-Forwarded-Host")
|
return r.Header.Get("X-Forwarded-Host")
|
||||||
default:
|
|
||||||
return r.Host
|
|
||||||
}
|
}
|
||||||
|
return r.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
// cookieDomain returns the fully qualified domain name of the request host
|
// cookieDomain returns the fully qualified domain name of the request host
|
||||||
|
@ -45,7 +53,7 @@ func cookieDomain(r *http.Request) string {
|
||||||
return strutils.JoinRune(parts, '.')
|
return strutils.JoinRune(parts, '.')
|
||||||
}
|
}
|
||||||
|
|
||||||
func setTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {
|
func SetTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: value,
|
Value: value,
|
||||||
|
@ -58,7 +66,7 @@ func setTokenCookie(w http.ResponseWriter, r *http.Request, name, value string,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearTokenCookie(w http.ResponseWriter, r *http.Request, name string) {
|
func ClearTokenCookie(w http.ResponseWriter, r *http.Request, name string) {
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: "",
|
Value: "",
|
||||||
|
|
|
@ -5,42 +5,54 @@ import (
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/certcrypto"
|
"github.com/go-acme/lego/v4/certcrypto"
|
||||||
|
"github.com/go-acme/lego/v4/challenge"
|
||||||
"github.com/go-acme/lego/v4/lego"
|
"github.com/go-acme/lego/v4/lego"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"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/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
"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 ProviderOpt `json:"options,omitempty"`
|
CADirURL string `json:"ca_dir_url,omitempty"`
|
||||||
|
CACerts []string `json:"ca_certs,omitempty"`
|
||||||
|
Options map[string]any `json:"options,omitempty"`
|
||||||
|
|
||||||
|
HTTPClient *http.Client `json:"-"` // for tests only
|
||||||
|
|
||||||
|
challengeProvider challenge.Provider
|
||||||
}
|
}
|
||||||
ProviderOpt map[string]any
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrMissingDomain = gperr.New("missing field 'domains'")
|
ErrMissingDomain = gperr.New("missing field 'domains'")
|
||||||
ErrMissingEmail = gperr.New("missing field 'email'")
|
ErrMissingEmail = gperr.New("missing field 'email'")
|
||||||
ErrMissingProvider = gperr.New("missing field 'provider'")
|
ErrMissingProvider = gperr.New("missing field 'provider'")
|
||||||
|
ErrMissingCADirURL = gperr.New("missing field 'ca_dir_url'")
|
||||||
ErrInvalidDomain = gperr.New("invalid domain")
|
ErrInvalidDomain = gperr.New("invalid domain")
|
||||||
ErrUnknownProvider = gperr.New("unknown provider")
|
ErrUnknownProvider = gperr.New("unknown provider")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProviderLocal = "local"
|
||||||
|
ProviderPseudo = "pseudo"
|
||||||
|
ProviderCustom = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -51,6 +63,10 @@ func (cfg *AutocertConfig) Validate() gperr.Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
b := gperr.NewBuilder("autocert errors")
|
b := gperr.NewBuilder("autocert errors")
|
||||||
|
if cfg.Provider == ProviderCustom && cfg.CADirURL == "" {
|
||||||
|
b.Add(ErrMissingCADirURL)
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Provider != ProviderLocal && cfg.Provider != ProviderPseudo {
|
if cfg.Provider != ProviderLocal && cfg.Provider != ProviderPseudo {
|
||||||
if len(cfg.Domains) == 0 {
|
if len(cfg.Domains) == 0 {
|
||||||
b.Add(ErrMissingDomain)
|
b.Add(ErrMissingDomain)
|
||||||
|
@ -58,34 +74,40 @@ func (cfg *AutocertConfig) Validate() gperr.Error {
|
||||||
if cfg.Email == "" {
|
if cfg.Email == "" {
|
||||||
b.Add(ErrMissingEmail)
|
b.Add(ErrMissingEmail)
|
||||||
}
|
}
|
||||||
|
if cfg.Provider != ProviderCustom {
|
||||||
for i, d := range cfg.Domains {
|
for i, d := range cfg.Domains {
|
||||||
if !domainOrWildcardRE.MatchString(d) {
|
if !domainOrWildcardRE.MatchString(d) {
|
||||||
b.Add(ErrInvalidDomain.Subjectf("domains[%d]", i))
|
b.Add(ErrInvalidDomain.Subjectf("domains[%d]", i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// check if provider is implemented
|
// check if provider is implemented
|
||||||
providerConstructor, ok := providers[cfg.Provider]
|
providerConstructor, ok := Providers[cfg.Provider]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
if cfg.Provider != ProviderCustom {
|
||||||
b.Add(ErrUnknownProvider.
|
b.Add(ErrUnknownProvider.
|
||||||
Subject(cfg.Provider).
|
Subject(cfg.Provider).
|
||||||
Withf(strutils.DoYouMean(utils.NearestField(cfg.Provider, providers))))
|
With(gperr.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_, err := providerConstructor(cfg.Options)
|
provider, err := providerConstructor(cfg.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Add(err)
|
b.Add(err)
|
||||||
|
} else {
|
||||||
|
cfg.challengeProvider = provider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.challengeProvider == nil {
|
||||||
|
cfg.challengeProvider, _ = Providers[ProviderLocal](nil)
|
||||||
|
}
|
||||||
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 +124,49 @@ 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")
|
log.Info().Err(err).Msg("failed to load ACME private key, generating a now one")
|
||||||
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.EC256
|
||||||
|
|
||||||
return &Provider{
|
if cfg.HTTPClient != nil {
|
||||||
cfg: cfg,
|
legoCfg.HTTPClient = cfg.HTTPClient
|
||||||
user: user,
|
|
||||||
legoCfg: legoCfg,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AutocertConfig) loadACMEKey() (*ecdsa.PrivateKey, error) {
|
if cfg.CADirURL != "" {
|
||||||
|
legoCfg.CADirURL = cfg.CADirURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.CACerts) > 0 {
|
||||||
|
certPool, err := lego.CreateCertPool(cfg.CACerts, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, gperr.New("failed to create cert pool").With(err)
|
||||||
|
}
|
||||||
|
legoCfg.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = certPool
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, legoCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) LoadACMEKey() (*ecdsa.PrivateKey, error) {
|
||||||
|
if common.IsTest {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
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 +174,10 @@ 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 {
|
||||||
|
if common.IsTest {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
data, err := x509.MarshalECPrivateKey(key)
|
data, err := x509.MarshalECPrivateKey(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -5,4 +5,5 @@ const (
|
||||||
CertFileDefault = certBasePath + "cert.crt"
|
CertFileDefault = certBasePath + "cert.crt"
|
||||||
KeyFileDefault = certBasePath + "priv.key"
|
KeyFileDefault = certBasePath + "priv.key"
|
||||||
ACMEKeyFileDefault = certBasePath + "acme.key"
|
ACMEKeyFileDefault = certBasePath + "acme.key"
|
||||||
|
LastFailureFile = certBasePath + ".last_failure"
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,44 +5,58 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"slices"
|
||||||
"sort"
|
"strings"
|
||||||
"sync"
|
|
||||||
"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/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"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/notif"
|
||||||
"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
|
||||||
|
lastFailure time.Time
|
||||||
|
|
||||||
legoCert *certificate.Resource
|
legoCert *certificate.Resource
|
||||||
tlsCert *tls.Certificate
|
tlsCert *tls.Certificate
|
||||||
certExpiries CertExpiries
|
certExpiries CertExpiries
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
|
const (
|
||||||
|
// renew failed for whatever reason, 1 hour cooldown
|
||||||
|
renewalCooldownDuration = 1 * time.Hour
|
||||||
|
// prevents cert request docker compose across restarts with `restart: always` (non-zero exit code)
|
||||||
|
requestCooldownDuration = 15 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -66,28 +80,64 @@ func (p *Provider) GetExpiries() CertExpiries {
|
||||||
return p.certExpiries
|
return p.certExpiries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) GetLastFailure() (time.Time, error) {
|
||||||
|
if p.lastFailure.IsZero() {
|
||||||
|
data, err := os.ReadFile(LastFailureFile)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.lastFailure, _ = time.Parse(time.RFC3339, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p.lastFailure, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) UpdateLastFailure() error {
|
||||||
|
t := time.Now()
|
||||||
|
p.lastFailure = t
|
||||||
|
return os.WriteFile(LastFailureFile, t.AppendFormat(nil, time.RFC3339), 0o600)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) ClearLastFailure() error {
|
||||||
|
p.lastFailure = time.Time{}
|
||||||
|
return os.Remove(LastFailureFile)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Provider) ObtainCert() error {
|
func (p *Provider) ObtainCert() error {
|
||||||
if p.cfg.Provider == ProviderLocal {
|
if p.cfg.Provider == ProviderLocal {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.cfg.Provider == ProviderPseudo {
|
if p.cfg.Provider == ProviderPseudo {
|
||||||
t := time.NewTicker(1000 * time.Millisecond)
|
log.Info().Msg("init client for pseudo provider")
|
||||||
defer t.Stop()
|
<-time.After(time.Second)
|
||||||
logging.Info().Msg("init client for pseudo provider")
|
log.Info().Msg("registering acme for pseudo provider")
|
||||||
<-t.C
|
<-time.After(time.Second)
|
||||||
logging.Info().Msg("registering acme for pseudo provider")
|
log.Info().Msg("obtained cert for pseudo provider")
|
||||||
<-t.C
|
|
||||||
logging.Info().Msg("obtained cert for pseudo provider")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lastFailure, err := p.GetLastFailure(); err != nil {
|
||||||
|
return err
|
||||||
|
} else if time.Since(lastFailure) < requestCooldownDuration {
|
||||||
|
return fmt.Errorf("%w: still in cooldown until %s", ErrGetCertFailure, strutils.FormatTime(lastFailure.Add(requestCooldownDuration).Local()))
|
||||||
|
}
|
||||||
|
|
||||||
if p.client == nil {
|
if p.client == nil {
|
||||||
if err := p.initClient(); err != nil {
|
if err := p.initClient(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mark it as failed first, clear it later if successful
|
||||||
|
// in case the process crashed / failed to renew, we put it on a cooldown
|
||||||
|
// this prevents rate limiting by the ACME server
|
||||||
|
if err := p.UpdateLastFailure(); err != nil {
|
||||||
|
return fmt.Errorf("failed to update last failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if p.user.Registration == nil {
|
if p.user.Registration == nil {
|
||||||
if err := p.registerACME(); err != nil {
|
if err := p.registerACME(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -103,7 +153,7 @@ func (p *Provider) ObtainCert() error {
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.legoCert = nil
|
p.legoCert = nil
|
||||||
logging.Err(err).Msg("cert renew failed, fallback to obtain")
|
log.Err(err).Msg("cert renew failed, fallback to obtain")
|
||||||
} else {
|
} else {
|
||||||
p.legoCert = cert
|
p.legoCert = cert
|
||||||
}
|
}
|
||||||
|
@ -135,6 +185,9 @@ func (p *Provider) ObtainCert() error {
|
||||||
p.tlsCert = &tlsCert
|
p.tlsCert = &tlsCert
|
||||||
p.certExpiries = expiries
|
p.certExpiries = expiries
|
||||||
|
|
||||||
|
if err := p.ClearLastFailure(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clear last failure: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +203,7 @@ func (p *Provider) LoadCert() error {
|
||||||
p.tlsCert = &cert
|
p.tlsCert = &cert
|
||||||
p.certExpiries = expiries
|
p.certExpiries = expiries
|
||||||
|
|
||||||
logging.Info().Msgf("next renewal in %v", strutils.FormatDuration(time.Until(p.ShouldRenewOn())))
|
log.Info().Msgf("next cert renewal in %s", strutils.FormatDuration(time.Until(p.ShouldRenewOn())))
|
||||||
return p.renewIfNeeded()
|
return p.renewIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,12 +221,11 @@ func (p *Provider) ScheduleRenewal(parent task.Parent) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
lastErrOn := time.Time{}
|
|
||||||
renewalTime := p.ShouldRenewOn()
|
renewalTime := p.ShouldRenewOn()
|
||||||
timer := time.NewTimer(time.Until(renewalTime))
|
timer := time.NewTimer(time.Until(renewalTime))
|
||||||
defer timer.Stop()
|
defer timer.Stop()
|
||||||
|
|
||||||
task := parent.Subtask("cert-renew-scheduler")
|
task := parent.Subtask("cert-renew-scheduler", true)
|
||||||
defer task.Finish(nil)
|
defer task.Finish(nil)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
@ -182,16 +234,35 @@ func (p *Provider) ScheduleRenewal(parent task.Parent) {
|
||||||
return
|
return
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
// Retry after 1 hour on failure
|
// Retry after 1 hour on failure
|
||||||
if !lastErrOn.IsZero() && time.Now().Before(lastErrOn.Add(time.Hour)) {
|
lastFailure, err := p.GetLastFailure()
|
||||||
|
if err != nil {
|
||||||
|
gperr.LogWarn("autocert: failed to get last failure", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !lastFailure.IsZero() && time.Since(lastFailure) < renewalCooldownDuration {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := p.renewIfNeeded(); err != nil {
|
if err := p.renewIfNeeded(); err != nil {
|
||||||
gperr.LogWarn("cert renew failed", err)
|
gperr.LogWarn("autocert: cert renew failed", err)
|
||||||
lastErrOn = time.Now()
|
if err := p.UpdateLastFailure(); err != nil {
|
||||||
|
gperr.LogWarn("autocert: failed to update last failure", err)
|
||||||
|
}
|
||||||
|
notif.Notify(¬if.LogMessage{
|
||||||
|
Level: zerolog.ErrorLevel,
|
||||||
|
Title: "SSL certificate renewal failed",
|
||||||
|
Body: notif.MessageBody(err.Error()),
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
notif.Notify(¬if.LogMessage{
|
||||||
|
Level: zerolog.InfoLevel,
|
||||||
|
Title: "SSL certificate renewed",
|
||||||
|
Body: notif.ListBody(p.cfg.Domains),
|
||||||
|
})
|
||||||
// Reset on success
|
// Reset on success
|
||||||
lastErrOn = time.Time{}
|
if err := p.ClearLastFailure(); err != nil {
|
||||||
|
gperr.LogWarn("autocert: failed to clear last failure", err)
|
||||||
|
}
|
||||||
renewalTime = p.ShouldRenewOn()
|
renewalTime = p.ShouldRenewOn()
|
||||||
timer.Reset(time.Until(renewalTime))
|
timer.Reset(time.Until(renewalTime))
|
||||||
}
|
}
|
||||||
|
@ -205,13 +276,7 @@ func (p *Provider) initClient() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
generator := providers[p.cfg.Provider]
|
err = legoClient.Challenge.SetDNS01Provider(p.cfg.challengeProvider)
|
||||||
legoProvider, pErr := generator(p.cfg.Options)
|
|
||||||
if pErr != nil {
|
|
||||||
return pErr
|
|
||||||
}
|
|
||||||
|
|
||||||
err = legoClient.Challenge.SetDNS01Provider(legoProvider)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -226,7 +291,7 @@ func (p *Provider) registerACME() error {
|
||||||
}
|
}
|
||||||
if reg, err := p.client.Registration.ResolveAccountByKey(); err == nil {
|
if reg, err := p.client.Registration.ResolveAccountByKey(); err == nil {
|
||||||
p.user.Registration = reg
|
p.user.Registration = reg
|
||||||
logging.Info().Msg("reused acme registration from private key")
|
log.Info().Msg("reused acme registration from private key")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,11 +300,14 @@ func (p *Provider) registerACME() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.user.Registration = reg
|
p.user.Registration = reg
|
||||||
logging.Info().Interface("reg", reg).Msg("acme registered")
|
log.Info().Interface("reg", reg).Msg("acme registered")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) saveCert(cert *certificate.Resource) error {
|
func (p *Provider) saveCert(cert *certificate.Resource) error {
|
||||||
|
if common.IsTest {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
/* This should have been done in setup
|
/* This should have been done in setup
|
||||||
but double check is always a good choice.*/
|
but double check is always a good choice.*/
|
||||||
_, err := os.Stat(path.Dir(p.cfg.CertPath))
|
_, err := os.Stat(path.Dir(p.cfg.CertPath))
|
||||||
|
@ -269,22 +337,19 @@ func (p *Provider) certState() CertState {
|
||||||
return CertStateExpired
|
return CertStateExpired
|
||||||
}
|
}
|
||||||
|
|
||||||
certDomains := make([]string, len(p.certExpiries))
|
if len(p.certExpiries) != len(p.cfg.Domains) {
|
||||||
wantedDomains := make([]string, len(p.cfg.Domains))
|
|
||||||
i := 0
|
|
||||||
for domain := range p.certExpiries {
|
|
||||||
certDomains[i] = domain
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
copy(wantedDomains, p.cfg.Domains)
|
|
||||||
sort.Strings(wantedDomains)
|
|
||||||
sort.Strings(certDomains)
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(certDomains, wantedDomains) {
|
|
||||||
logging.Info().Msgf("cert domains mismatch: %v != %v", certDomains, p.cfg.Domains)
|
|
||||||
return CertStateMismatch
|
return CertStateMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range len(p.cfg.Domains) {
|
||||||
|
if _, ok := p.certExpiries[p.cfg.Domains[i]]; !ok {
|
||||||
|
log.Info().Msgf("autocert domains mismatch: cert: %s, wanted: %s",
|
||||||
|
strings.Join(slices.Collect(maps.Keys(p.certExpiries)), ", "),
|
||||||
|
strings.Join(p.cfg.Domains, ", "))
|
||||||
|
return CertStateMismatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return CertStateValid
|
return CertStateValid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,9 +360,9 @@ func (p *Provider) renewIfNeeded() error {
|
||||||
|
|
||||||
switch p.certState() {
|
switch p.certState() {
|
||||||
case CertStateExpired:
|
case CertStateExpired:
|
||||||
logging.Info().Msg("certs expired, renewing")
|
log.Info().Msg("certs expired, renewing")
|
||||||
case CertStateMismatch:
|
case CertStateMismatch:
|
||||||
logging.Info().Msg("cert domains mismatch with config, renewing")
|
log.Info().Msg("cert domains mismatch with config, renewing")
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -322,18 +387,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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
468
internal/autocert/provider_test/custom_test.go
Normal file
468
internal/autocert/provider_test/custom_test.go
Normal file
|
@ -0,0 +1,468 @@
|
||||||
|
//nolint:errchkjson,errcheck
|
||||||
|
package provider_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/go-proxy/internal/autocert"
|
||||||
|
"github.com/yusing/go-proxy/internal/dnsproviders"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
dnsproviders.InitProviders()
|
||||||
|
m.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomProvider(t *testing.T) {
|
||||||
|
t.Run("valid custom provider with step-ca", func(t *testing.T) {
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Email: "test@example.com",
|
||||||
|
Domains: []string{"example.com", "*.example.com"},
|
||||||
|
Provider: autocert.ProviderCustom,
|
||||||
|
CADirURL: "https://ca.example.com:9000/acme/acme/directory",
|
||||||
|
CertPath: "certs/custom.crt",
|
||||||
|
KeyPath: "certs/custom.key",
|
||||||
|
ACMEKeyPath: "certs/custom-acme.key",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cfg.Validate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
user, legoCfg, err := cfg.GetLegoConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, user)
|
||||||
|
require.NotNil(t, legoCfg)
|
||||||
|
require.Equal(t, "https://ca.example.com:9000/acme/acme/directory", legoCfg.CADirURL)
|
||||||
|
require.Equal(t, "test@example.com", user.Email)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("custom provider missing CADirURL", func(t *testing.T) {
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Email: "test@example.com",
|
||||||
|
Domains: []string{"example.com"},
|
||||||
|
Provider: autocert.ProviderCustom,
|
||||||
|
// CADirURL is missing
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cfg.Validate()
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "missing field 'ca_dir_url'")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("custom provider with step-ca internal CA", func(t *testing.T) {
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Email: "admin@internal.com",
|
||||||
|
Domains: []string{"internal.example.com", "api.internal.example.com"},
|
||||||
|
Provider: autocert.ProviderCustom,
|
||||||
|
CADirURL: "https://step-ca.internal:443/acme/acme/directory",
|
||||||
|
CertPath: "certs/internal.crt",
|
||||||
|
KeyPath: "certs/internal.key",
|
||||||
|
ACMEKeyPath: "certs/internal-acme.key",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cfg.Validate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
user, legoCfg, err := cfg.GetLegoConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, user)
|
||||||
|
require.NotNil(t, legoCfg)
|
||||||
|
require.Equal(t, "https://step-ca.internal:443/acme/acme/directory", legoCfg.CADirURL)
|
||||||
|
require.Equal(t, "admin@internal.com", user.Email)
|
||||||
|
|
||||||
|
provider := autocert.NewProvider(cfg, user, legoCfg)
|
||||||
|
require.NotNil(t, provider)
|
||||||
|
require.Equal(t, autocert.ProviderCustom, provider.GetName())
|
||||||
|
require.Equal(t, "certs/internal.crt", provider.GetCertPath())
|
||||||
|
require.Equal(t, "certs/internal.key", provider.GetKeyPath())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObtainCertFromCustomProvider(t *testing.T) {
|
||||||
|
// Create a test ACME server
|
||||||
|
acmeServer := newTestACMEServer(t)
|
||||||
|
defer acmeServer.Close()
|
||||||
|
|
||||||
|
t.Run("obtain cert from custom step-ca server", func(t *testing.T) {
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Email: "test@example.com",
|
||||||
|
Domains: []string{"test.example.com"},
|
||||||
|
Provider: autocert.ProviderCustom,
|
||||||
|
CADirURL: acmeServer.URL() + "/acme/acme/directory",
|
||||||
|
CertPath: "certs/stepca-test.crt",
|
||||||
|
KeyPath: "certs/stepca-test.key",
|
||||||
|
ACMEKeyPath: "certs/stepca-test-acme.key",
|
||||||
|
HTTPClient: acmeServer.httpClient(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := error(cfg.Validate())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
user, legoCfg, err := cfg.GetLegoConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, user)
|
||||||
|
require.NotNil(t, legoCfg)
|
||||||
|
|
||||||
|
provider := autocert.NewProvider(cfg, user, legoCfg)
|
||||||
|
require.NotNil(t, provider)
|
||||||
|
|
||||||
|
// Test obtaining certificate
|
||||||
|
err = provider.ObtainCert()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify certificate was obtained
|
||||||
|
cert, err := provider.GetCert(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, cert)
|
||||||
|
|
||||||
|
// Verify certificate properties
|
||||||
|
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, x509Cert.DNSNames, "test.example.com")
|
||||||
|
require.True(t, time.Now().Before(x509Cert.NotAfter))
|
||||||
|
require.True(t, time.Now().After(x509Cert.NotBefore))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// testACMEServer implements a minimal ACME server for testing.
|
||||||
|
type testACMEServer struct {
|
||||||
|
server *httptest.Server
|
||||||
|
caCert *x509.Certificate
|
||||||
|
caKey *rsa.PrivateKey
|
||||||
|
clientCSRs map[string]*x509.CertificateRequest
|
||||||
|
orderID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestACMEServer(t *testing.T) *testACMEServer {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Generate CA certificate and key
|
||||||
|
caKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
caTemplate := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(1),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: []string{"Test CA"},
|
||||||
|
Country: []string{"US"},
|
||||||
|
Province: []string{""},
|
||||||
|
Locality: []string{"Test"},
|
||||||
|
StreetAddress: []string{""},
|
||||||
|
PostalCode: []string{""},
|
||||||
|
},
|
||||||
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||||
|
IsCA: true,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
caCert, err := x509.ParseCertificate(caCertDER)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
acme := &testACMEServer{
|
||||||
|
caCert: caCert,
|
||||||
|
caKey: caKey,
|
||||||
|
clientCSRs: make(map[string]*x509.CertificateRequest),
|
||||||
|
orderID: "test-order-123",
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
acme.setupRoutes(mux)
|
||||||
|
|
||||||
|
acme.server = httptest.NewUnstartedServer(mux)
|
||||||
|
acme.server.TLS = &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{
|
||||||
|
{
|
||||||
|
Certificate: [][]byte{caCert.Raw},
|
||||||
|
PrivateKey: caKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
acme.server.StartTLS()
|
||||||
|
return acme
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) Close() {
|
||||||
|
s.server.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) URL() string {
|
||||||
|
return s.server.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) httpClient() *http.Client {
|
||||||
|
certPool := x509.NewCertPool()
|
||||||
|
certPool.AddCert(s.caCert)
|
||||||
|
|
||||||
|
return &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
TLSHandshakeTimeout: 30 * time.Second,
|
||||||
|
ResponseHeaderTimeout: 30 * time.Second,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
RootCAs: certPool,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) setupRoutes(mux *http.ServeMux) {
|
||||||
|
// ACME directory endpoint
|
||||||
|
mux.HandleFunc("/acme/acme/directory", s.handleDirectory)
|
||||||
|
|
||||||
|
// ACME endpoints
|
||||||
|
mux.HandleFunc("/acme/new-nonce", s.handleNewNonce)
|
||||||
|
mux.HandleFunc("/acme/new-account", s.handleNewAccount)
|
||||||
|
mux.HandleFunc("/acme/new-order", s.handleNewOrder)
|
||||||
|
mux.HandleFunc("/acme/authz/", s.handleAuthorization)
|
||||||
|
mux.HandleFunc("/acme/chall/", s.handleChallenge)
|
||||||
|
mux.HandleFunc("/acme/order/", s.handleOrder)
|
||||||
|
mux.HandleFunc("/acme/cert/", s.handleCertificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) handleDirectory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
directory := map[string]interface{}{
|
||||||
|
"newNonce": s.server.URL + "/acme/new-nonce",
|
||||||
|
"newAccount": s.server.URL + "/acme/new-account",
|
||||||
|
"newOrder": s.server.URL + "/acme/new-order",
|
||||||
|
"keyChange": s.server.URL + "/acme/key-change",
|
||||||
|
"meta": map[string]interface{}{
|
||||||
|
"termsOfService": s.server.URL + "/terms",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(directory)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) handleNewNonce(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Replay-Nonce", "test-nonce-12345")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) handleNewAccount(w http.ResponseWriter, r *http.Request) {
|
||||||
|
account := map[string]interface{}{
|
||||||
|
"status": "valid",
|
||||||
|
"contact": []string{"mailto:test@example.com"},
|
||||||
|
"orders": s.server.URL + "/acme/orders",
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Location", s.server.URL+"/acme/account/1")
|
||||||
|
w.Header().Set("Replay-Nonce", "test-nonce-67890")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) handleNewOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
authzID := "test-authz-456"
|
||||||
|
|
||||||
|
order := map[string]interface{}{
|
||||||
|
"status": "ready", // Skip pending state for simplicity
|
||||||
|
"expires": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
||||||
|
"identifiers": []map[string]string{{"type": "dns", "value": "test.example.com"}},
|
||||||
|
"authorizations": []string{s.server.URL + "/acme/authz/" + authzID},
|
||||||
|
"finalize": s.server.URL + "/acme/order/" + s.orderID + "/finalize",
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Location", s.server.URL+"/acme/order/"+s.orderID)
|
||||||
|
w.Header().Set("Replay-Nonce", "test-nonce-order")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(order)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) handleAuthorization(w http.ResponseWriter, r *http.Request) {
|
||||||
|
authz := map[string]interface{}{
|
||||||
|
"status": "valid", // Skip challenge validation for simplicity
|
||||||
|
"expires": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
||||||
|
"identifier": map[string]string{"type": "dns", "value": "test.example.com"},
|
||||||
|
"challenges": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"type": "dns-01",
|
||||||
|
"status": "valid",
|
||||||
|
"url": s.server.URL + "/acme/chall/test-chall-789",
|
||||||
|
"token": "test-token-abc123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Replay-Nonce", "test-nonce-authz")
|
||||||
|
json.NewEncoder(w).Encode(authz)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) handleChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
|
challenge := map[string]interface{}{
|
||||||
|
"type": "dns-01",
|
||||||
|
"status": "valid",
|
||||||
|
"url": r.URL.String(),
|
||||||
|
"token": "test-token-abc123",
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Replay-Nonce", "test-nonce-chall")
|
||||||
|
json.NewEncoder(w).Encode(challenge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) handleOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/finalize") {
|
||||||
|
s.handleFinalize(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
certURL := s.server.URL + "/acme/cert/" + s.orderID
|
||||||
|
order := map[string]interface{}{
|
||||||
|
"status": "valid",
|
||||||
|
"expires": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
||||||
|
"identifiers": []map[string]string{{"type": "dns", "value": "test.example.com"}},
|
||||||
|
"certificate": certURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Replay-Nonce", "test-nonce-order-get")
|
||||||
|
json.NewEncoder(w).Encode(order)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) handleFinalize(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Read the JWS payload
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to read request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract CSR from JWS payload
|
||||||
|
csr, err := s.extractCSRFromJWS(body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid CSR: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the CSR for certificate generation
|
||||||
|
s.clientCSRs[s.orderID] = csr
|
||||||
|
|
||||||
|
certURL := s.server.URL + "/acme/cert/" + s.orderID
|
||||||
|
order := map[string]interface{}{
|
||||||
|
"status": "valid",
|
||||||
|
"expires": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
||||||
|
"identifiers": []map[string]string{{"type": "dns", "value": "test.example.com"}},
|
||||||
|
"certificate": certURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Location", strings.TrimSuffix(r.URL.String(), "/finalize"))
|
||||||
|
w.Header().Set("Replay-Nonce", "test-nonce-finalize")
|
||||||
|
json.NewEncoder(w).Encode(order)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) extractCSRFromJWS(jwsData []byte) (*x509.CertificateRequest, error) {
|
||||||
|
// Parse the JWS structure
|
||||||
|
var jws struct {
|
||||||
|
Protected string `json:"protected"`
|
||||||
|
Payload string `json:"payload"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(jwsData, &jws); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the payload
|
||||||
|
payloadBytes, err := base64.RawURLEncoding.DecodeString(jws.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the finalize request
|
||||||
|
var finalizeReq struct {
|
||||||
|
CSR string `json:"csr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(payloadBytes, &finalizeReq); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the CSR
|
||||||
|
csrBytes, err := base64.RawURLEncoding.DecodeString(finalizeReq.CSR)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the CSR
|
||||||
|
csr, err := x509.ParseCertificateRequest(csrBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return csr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testACMEServer) handleCertificate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Extract order ID from URL
|
||||||
|
orderID := strings.TrimPrefix(r.URL.Path, "/acme/cert/")
|
||||||
|
|
||||||
|
// Get the CSR for this order
|
||||||
|
csr, exists := s.clientCSRs[orderID]
|
||||||
|
if !exists {
|
||||||
|
http.Error(w, "No CSR found for order", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create certificate using the public key from the client's CSR
|
||||||
|
template := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(2),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Organization: []string{"Test Cert"},
|
||||||
|
Country: []string{"US"},
|
||||||
|
},
|
||||||
|
DNSNames: csr.DNSNames,
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(90 * 24 * time.Hour),
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the public key from the CSR and sign with CA key
|
||||||
|
certDER, err := x509.CreateCertificate(rand.Reader, template, s.caCert, csr.PublicKey, s.caKey)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return certificate chain
|
||||||
|
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||||
|
caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: s.caCert.Raw})
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/pem-certificate-chain")
|
||||||
|
w.Header().Set("Replay-Nonce", "test-nonce-cert")
|
||||||
|
w.Write(append(certPEM, caPEM...))
|
||||||
|
}
|
|
@ -5,8 +5,8 @@ import (
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||||
"github.com/goccy/go-yaml"
|
"github.com/goccy/go-yaml"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
"github.com/stretchr/testify/require"
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
"github.com/yusing/go-proxy/internal/serialization"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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, serialization.MapUnmarshalValidate(opt, cfg))
|
||||||
ExpectEqual(t, cfg, cfgExpected)
|
require.Equal(t, cfgExpected, cfg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,452 +1,28 @@
|
||||||
//go:generate /usr/bin/python3 gen.py
|
|
||||||
|
|
||||||
package autocert
|
package autocert
|
||||||
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/acmedns"
|
import (
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/active24"
|
"github.com/go-acme/lego/v4/challenge"
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/alidns"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/allinkl"
|
"github.com/yusing/go-proxy/internal/serialization"
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/arvancloud"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/auroradns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/autodns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/axelname"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/azuredns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/baiducloud"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/bindman"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/bluecat"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/bookmyname"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/bunny"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/checkdomain"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/civo"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/clouddns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/cloudns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/cloudru"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/conoha"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/constellix"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/corenetworks"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/cpanel"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/derak"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/desec"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/designate"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/digitalocean"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/directadmin"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/dnshomede"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/dnsimple"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/dode"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/domeneshop"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/dreamhost"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/duckdns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/dyn"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/dynu"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/easydns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/edgedns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/efficientip"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/epik"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/exec"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/exoscale"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/f5xc"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/freemyip"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/gandi"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/gandiv5"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/gcloud"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/gcore"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/glesys"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/godaddy"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/googledomains"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/hetzner"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/hostingde"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/hosttech"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/httpnet"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/httpreq"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/hurricane"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/hyperone"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/ibmcloud"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/iij"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/iijdpf"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/infoblox"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/infomaniak"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/internetbs"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/inwx"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/ionos"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/ipv64"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/iwantmyname"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/joker"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/liara"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/lightsail"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/limacity"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/linode"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/liquidweb"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/loopia"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/luadns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/mailinabox"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/manageengine"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/metaname"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/metaregistrar"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/mijnhost"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/mittwald"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/myaddr"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/mydnsjp"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/namecheap"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/namedotcom"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/namesilo"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/netcup"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/netlify"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/nicmanager"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/nifcloud"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/njalla"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/nodion"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/ns1"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/oraclecloud"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/otc"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/ovh"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/pdns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/plesk"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/porkbun"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/rackspace"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/rainyun"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/rcodezero"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/regfish"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/regru"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/rfc2136"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/rimuhosting"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/route53"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/safedns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/sakuracloud"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/scaleway"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/selectel"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/selectelv2"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/selfhostde"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/servercow"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/shellrent"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/simply"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/sonic"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/spaceship"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/stackpath"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/technitium"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/timewebcloud"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/transip"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/ultradns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/variomedia"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/vegadns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/vercel"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/versio"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/vinyldns"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/vkcloud"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/volcengine"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/vscale"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/vultr"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/webnames"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/websupport"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/wedos"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/westcn"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/yandex"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/yandex360"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/zoneee"
|
|
||||||
import "github.com/go-acme/lego/v4/providers/dns/zonomi"
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProviderLocal = "local"
|
|
||||||
ProviderPseudo = "pseudo"
|
|
||||||
Provideracmedns = "acmedns"
|
|
||||||
Provideractive24 = "active24"
|
|
||||||
Provideralidns = "alidns"
|
|
||||||
Providerallinkl = "allinkl"
|
|
||||||
Providerarvancloud = "arvancloud"
|
|
||||||
Providerauroradns = "auroradns"
|
|
||||||
Providerautodns = "autodns"
|
|
||||||
Provideraxelname = "axelname"
|
|
||||||
Providerazuredns = "azuredns"
|
|
||||||
Providerbaiducloud = "baiducloud"
|
|
||||||
Providerbindman = "bindman"
|
|
||||||
Providerbluecat = "bluecat"
|
|
||||||
Providerbookmyname = "bookmyname"
|
|
||||||
Providerbunny = "bunny"
|
|
||||||
Providercheckdomain = "checkdomain"
|
|
||||||
Providercivo = "civo"
|
|
||||||
Providerclouddns = "clouddns"
|
|
||||||
Providercloudflare = "cloudflare"
|
|
||||||
Providercloudns = "cloudns"
|
|
||||||
Providercloudru = "cloudru"
|
|
||||||
Providerconoha = "conoha"
|
|
||||||
Providerconstellix = "constellix"
|
|
||||||
Providercorenetworks = "corenetworks"
|
|
||||||
Providercpanel = "cpanel"
|
|
||||||
Providerderak = "derak"
|
|
||||||
Providerdesec = "desec"
|
|
||||||
Providerdesignate = "designate"
|
|
||||||
Providerdigitalocean = "digitalocean"
|
|
||||||
Providerdirectadmin = "directadmin"
|
|
||||||
Providerdnshomede = "dnshomede"
|
|
||||||
Providerdnsimple = "dnsimple"
|
|
||||||
Providerdnsmadeeasy = "dnsmadeeasy"
|
|
||||||
Providerdode = "dode"
|
|
||||||
Providerdomeneshop = "domeneshop"
|
|
||||||
Providerdreamhost = "dreamhost"
|
|
||||||
Providerduckdns = "duckdns"
|
|
||||||
Providerdyn = "dyn"
|
|
||||||
Providerdynu = "dynu"
|
|
||||||
Providereasydns = "easydns"
|
|
||||||
Provideredgedns = "edgedns"
|
|
||||||
Providerefficientip = "efficientip"
|
|
||||||
Providerepik = "epik"
|
|
||||||
Providerexec = "exec"
|
|
||||||
Providerexoscale = "exoscale"
|
|
||||||
Providerf5xc = "f5xc"
|
|
||||||
Providerfreemyip = "freemyip"
|
|
||||||
Providergandi = "gandi"
|
|
||||||
Providergandiv5 = "gandiv5"
|
|
||||||
Providergcloud = "gcloud"
|
|
||||||
Providergcore = "gcore"
|
|
||||||
Providerglesys = "glesys"
|
|
||||||
Providergodaddy = "godaddy"
|
|
||||||
Providergoogledomains = "googledomains"
|
|
||||||
Providerhetzner = "hetzner"
|
|
||||||
Providerhostingde = "hostingde"
|
|
||||||
Providerhosttech = "hosttech"
|
|
||||||
Providerhttpnet = "httpnet"
|
|
||||||
Providerhttpreq = "httpreq"
|
|
||||||
Providerhuaweicloud = "huaweicloud"
|
|
||||||
Providerhurricane = "hurricane"
|
|
||||||
Providerhyperone = "hyperone"
|
|
||||||
Provideribmcloud = "ibmcloud"
|
|
||||||
Provideriij = "iij"
|
|
||||||
Provideriijdpf = "iijdpf"
|
|
||||||
Providerinfoblox = "infoblox"
|
|
||||||
Providerinfomaniak = "infomaniak"
|
|
||||||
Providerinternetbs = "internetbs"
|
|
||||||
Providerinwx = "inwx"
|
|
||||||
Providerionos = "ionos"
|
|
||||||
Provideripv64 = "ipv64"
|
|
||||||
Provideriwantmyname = "iwantmyname"
|
|
||||||
Providerjoker = "joker"
|
|
||||||
Providerliara = "liara"
|
|
||||||
Providerlightsail = "lightsail"
|
|
||||||
Providerlimacity = "limacity"
|
|
||||||
Providerlinode = "linode"
|
|
||||||
Providerliquidweb = "liquidweb"
|
|
||||||
Providerloopia = "loopia"
|
|
||||||
Providerluadns = "luadns"
|
|
||||||
Providermailinabox = "mailinabox"
|
|
||||||
Providermanageengine = "manageengine"
|
|
||||||
Providermetaname = "metaname"
|
|
||||||
Providermetaregistrar = "metaregistrar"
|
|
||||||
Providermijnhost = "mijnhost"
|
|
||||||
Providermittwald = "mittwald"
|
|
||||||
Providermyaddr = "myaddr"
|
|
||||||
Providermydnsjp = "mydnsjp"
|
|
||||||
Providernamecheap = "namecheap"
|
|
||||||
Providernamedotcom = "namedotcom"
|
|
||||||
Providernamesilo = "namesilo"
|
|
||||||
Providernearlyfreespeech = "nearlyfreespeech"
|
|
||||||
Providernetcup = "netcup"
|
|
||||||
Providernetlify = "netlify"
|
|
||||||
Providernicmanager = "nicmanager"
|
|
||||||
Providernifcloud = "nifcloud"
|
|
||||||
Providernjalla = "njalla"
|
|
||||||
Providernodion = "nodion"
|
|
||||||
Providerns1 = "ns1"
|
|
||||||
Provideroraclecloud = "oraclecloud"
|
|
||||||
Providerotc = "otc"
|
|
||||||
Providerovh = "ovh"
|
|
||||||
Providerpdns = "pdns"
|
|
||||||
Providerplesk = "plesk"
|
|
||||||
Providerporkbun = "porkbun"
|
|
||||||
Providerrackspace = "rackspace"
|
|
||||||
Providerrainyun = "rainyun"
|
|
||||||
Providerrcodezero = "rcodezero"
|
|
||||||
Providerregfish = "regfish"
|
|
||||||
Providerregru = "regru"
|
|
||||||
Providerrfc2136 = "rfc2136"
|
|
||||||
Providerrimuhosting = "rimuhosting"
|
|
||||||
Providerroute53 = "route53"
|
|
||||||
Providersafedns = "safedns"
|
|
||||||
Providersakuracloud = "sakuracloud"
|
|
||||||
Providerscaleway = "scaleway"
|
|
||||||
Providerselectel = "selectel"
|
|
||||||
Providerselectelv2 = "selectelv2"
|
|
||||||
Providerselfhostde = "selfhostde"
|
|
||||||
Providerservercow = "servercow"
|
|
||||||
Providershellrent = "shellrent"
|
|
||||||
Providersimply = "simply"
|
|
||||||
Providersonic = "sonic"
|
|
||||||
Providerspaceship = "spaceship"
|
|
||||||
Providerstackpath = "stackpath"
|
|
||||||
Providertechnitium = "technitium"
|
|
||||||
Providertencentcloud = "tencentcloud"
|
|
||||||
Providertimewebcloud = "timewebcloud"
|
|
||||||
Providertransip = "transip"
|
|
||||||
Providerultradns = "ultradns"
|
|
||||||
Providervariomedia = "variomedia"
|
|
||||||
Providervegadns = "vegadns"
|
|
||||||
Providervercel = "vercel"
|
|
||||||
Providerversio = "versio"
|
|
||||||
Providervinyldns = "vinyldns"
|
|
||||||
Providervkcloud = "vkcloud"
|
|
||||||
Providervolcengine = "volcengine"
|
|
||||||
Providervscale = "vscale"
|
|
||||||
Providervultr = "vultr"
|
|
||||||
Providerwebnames = "webnames"
|
|
||||||
Providerwebsupport = "websupport"
|
|
||||||
Providerwedos = "wedos"
|
|
||||||
Providerwestcn = "westcn"
|
|
||||||
Provideryandex = "yandex"
|
|
||||||
Provideryandex360 = "yandex360"
|
|
||||||
Providerzoneee = "zoneee"
|
|
||||||
Providerzonomi = "zonomi"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var providers = map[string]ProviderGenerator{
|
type Generator func(map[string]any) (challenge.Provider, gperr.Error)
|
||||||
ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
|
||||||
ProviderPseudo: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
var Providers = make(map[string]Generator)
|
||||||
Provideracmedns: providerGenerator(acmedns.NewDefaultConfig, acmedns.NewDNSProviderConfig),
|
|
||||||
Provideractive24: providerGenerator(active24.NewDefaultConfig, active24.NewDNSProviderConfig),
|
func DNSProvider[CT any, PT challenge.Provider](
|
||||||
Provideralidns: providerGenerator(alidns.NewDefaultConfig, alidns.NewDNSProviderConfig),
|
defaultCfg func() *CT,
|
||||||
Providerallinkl: providerGenerator(allinkl.NewDefaultConfig, allinkl.NewDNSProviderConfig),
|
newProvider func(*CT) (PT, error),
|
||||||
Providerarvancloud: providerGenerator(arvancloud.NewDefaultConfig, arvancloud.NewDNSProviderConfig),
|
) Generator {
|
||||||
Providerauroradns: providerGenerator(auroradns.NewDefaultConfig, auroradns.NewDNSProviderConfig),
|
return func(opt map[string]any) (challenge.Provider, gperr.Error) {
|
||||||
Providerautodns: providerGenerator(autodns.NewDefaultConfig, autodns.NewDNSProviderConfig),
|
cfg := defaultCfg()
|
||||||
Provideraxelname: providerGenerator(axelname.NewDefaultConfig, axelname.NewDNSProviderConfig),
|
if len(opt) > 0 {
|
||||||
Providerazuredns: providerGenerator(azuredns.NewDefaultConfig, azuredns.NewDNSProviderConfig),
|
err := serialization.MapUnmarshalValidate(opt, &cfg)
|
||||||
Providerbaiducloud: providerGenerator(baiducloud.NewDefaultConfig, baiducloud.NewDNSProviderConfig),
|
if err != nil {
|
||||||
Providerbindman: providerGenerator(bindman.NewDefaultConfig, bindman.NewDNSProviderConfig),
|
return nil, err
|
||||||
Providerbluecat: providerGenerator(bluecat.NewDefaultConfig, bluecat.NewDNSProviderConfig),
|
}
|
||||||
Providerbookmyname: providerGenerator(bookmyname.NewDefaultConfig, bookmyname.NewDNSProviderConfig),
|
}
|
||||||
Providerbunny: providerGenerator(bunny.NewDefaultConfig, bunny.NewDNSProviderConfig),
|
p, pErr := newProvider(cfg)
|
||||||
Providercheckdomain: providerGenerator(checkdomain.NewDefaultConfig, checkdomain.NewDNSProviderConfig),
|
return p, gperr.Wrap(pErr)
|
||||||
Providercivo: providerGenerator(civo.NewDefaultConfig, civo.NewDNSProviderConfig),
|
}
|
||||||
Providerclouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
|
||||||
Providercloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
|
||||||
Providercloudns: providerGenerator(cloudns.NewDefaultConfig, cloudns.NewDNSProviderConfig),
|
|
||||||
Providercloudru: providerGenerator(cloudru.NewDefaultConfig, cloudru.NewDNSProviderConfig),
|
|
||||||
Providerconoha: providerGenerator(conoha.NewDefaultConfig, conoha.NewDNSProviderConfig),
|
|
||||||
Providerconstellix: providerGenerator(constellix.NewDefaultConfig, constellix.NewDNSProviderConfig),
|
|
||||||
Providercorenetworks: providerGenerator(corenetworks.NewDefaultConfig, corenetworks.NewDNSProviderConfig),
|
|
||||||
Providercpanel: providerGenerator(cpanel.NewDefaultConfig, cpanel.NewDNSProviderConfig),
|
|
||||||
Providerderak: providerGenerator(derak.NewDefaultConfig, derak.NewDNSProviderConfig),
|
|
||||||
Providerdesec: providerGenerator(desec.NewDefaultConfig, desec.NewDNSProviderConfig),
|
|
||||||
Providerdesignate: providerGenerator(designate.NewDefaultConfig, designate.NewDNSProviderConfig),
|
|
||||||
Providerdigitalocean: providerGenerator(digitalocean.NewDefaultConfig, digitalocean.NewDNSProviderConfig),
|
|
||||||
Providerdirectadmin: providerGenerator(directadmin.NewDefaultConfig, directadmin.NewDNSProviderConfig),
|
|
||||||
Providerdnshomede: providerGenerator(dnshomede.NewDefaultConfig, dnshomede.NewDNSProviderConfig),
|
|
||||||
Providerdnsimple: providerGenerator(dnsimple.NewDefaultConfig, dnsimple.NewDNSProviderConfig),
|
|
||||||
Providerdnsmadeeasy: providerGenerator(dnsmadeeasy.NewDefaultConfig, dnsmadeeasy.NewDNSProviderConfig),
|
|
||||||
Providerdode: providerGenerator(dode.NewDefaultConfig, dode.NewDNSProviderConfig),
|
|
||||||
Providerdomeneshop: providerGenerator(domeneshop.NewDefaultConfig, domeneshop.NewDNSProviderConfig),
|
|
||||||
Providerdreamhost: providerGenerator(dreamhost.NewDefaultConfig, dreamhost.NewDNSProviderConfig),
|
|
||||||
Providerduckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
|
||||||
Providerdyn: providerGenerator(dyn.NewDefaultConfig, dyn.NewDNSProviderConfig),
|
|
||||||
Providerdynu: providerGenerator(dynu.NewDefaultConfig, dynu.NewDNSProviderConfig),
|
|
||||||
Providereasydns: providerGenerator(easydns.NewDefaultConfig, easydns.NewDNSProviderConfig),
|
|
||||||
Provideredgedns: providerGenerator(edgedns.NewDefaultConfig, edgedns.NewDNSProviderConfig),
|
|
||||||
Providerefficientip: providerGenerator(efficientip.NewDefaultConfig, efficientip.NewDNSProviderConfig),
|
|
||||||
Providerepik: providerGenerator(epik.NewDefaultConfig, epik.NewDNSProviderConfig),
|
|
||||||
Providerexec: providerGenerator(exec.NewDefaultConfig, exec.NewDNSProviderConfig),
|
|
||||||
Providerexoscale: providerGenerator(exoscale.NewDefaultConfig, exoscale.NewDNSProviderConfig),
|
|
||||||
Providerf5xc: providerGenerator(f5xc.NewDefaultConfig, f5xc.NewDNSProviderConfig),
|
|
||||||
Providerfreemyip: providerGenerator(freemyip.NewDefaultConfig, freemyip.NewDNSProviderConfig),
|
|
||||||
Providergandi: providerGenerator(gandi.NewDefaultConfig, gandi.NewDNSProviderConfig),
|
|
||||||
Providergandiv5: providerGenerator(gandiv5.NewDefaultConfig, gandiv5.NewDNSProviderConfig),
|
|
||||||
Providergcloud: providerGenerator(gcloud.NewDefaultConfig, gcloud.NewDNSProviderConfig),
|
|
||||||
Providergcore: providerGenerator(gcore.NewDefaultConfig, gcore.NewDNSProviderConfig),
|
|
||||||
Providerglesys: providerGenerator(glesys.NewDefaultConfig, glesys.NewDNSProviderConfig),
|
|
||||||
Providergodaddy: providerGenerator(godaddy.NewDefaultConfig, godaddy.NewDNSProviderConfig),
|
|
||||||
Providergoogledomains: providerGenerator(googledomains.NewDefaultConfig, googledomains.NewDNSProviderConfig),
|
|
||||||
Providerhetzner: providerGenerator(hetzner.NewDefaultConfig, hetzner.NewDNSProviderConfig),
|
|
||||||
Providerhostingde: providerGenerator(hostingde.NewDefaultConfig, hostingde.NewDNSProviderConfig),
|
|
||||||
Providerhosttech: providerGenerator(hosttech.NewDefaultConfig, hosttech.NewDNSProviderConfig),
|
|
||||||
Providerhttpnet: providerGenerator(httpnet.NewDefaultConfig, httpnet.NewDNSProviderConfig),
|
|
||||||
Providerhttpreq: providerGenerator(httpreq.NewDefaultConfig, httpreq.NewDNSProviderConfig),
|
|
||||||
Providerhuaweicloud: providerGenerator(huaweicloud.NewDefaultConfig, huaweicloud.NewDNSProviderConfig),
|
|
||||||
Providerhurricane: providerGenerator(hurricane.NewDefaultConfig, hurricane.NewDNSProviderConfig),
|
|
||||||
Providerhyperone: providerGenerator(hyperone.NewDefaultConfig, hyperone.NewDNSProviderConfig),
|
|
||||||
Provideribmcloud: providerGenerator(ibmcloud.NewDefaultConfig, ibmcloud.NewDNSProviderConfig),
|
|
||||||
Provideriij: providerGenerator(iij.NewDefaultConfig, iij.NewDNSProviderConfig),
|
|
||||||
Provideriijdpf: providerGenerator(iijdpf.NewDefaultConfig, iijdpf.NewDNSProviderConfig),
|
|
||||||
Providerinfoblox: providerGenerator(infoblox.NewDefaultConfig, infoblox.NewDNSProviderConfig),
|
|
||||||
Providerinfomaniak: providerGenerator(infomaniak.NewDefaultConfig, infomaniak.NewDNSProviderConfig),
|
|
||||||
Providerinternetbs: providerGenerator(internetbs.NewDefaultConfig, internetbs.NewDNSProviderConfig),
|
|
||||||
Providerinwx: providerGenerator(inwx.NewDefaultConfig, inwx.NewDNSProviderConfig),
|
|
||||||
Providerionos: providerGenerator(ionos.NewDefaultConfig, ionos.NewDNSProviderConfig),
|
|
||||||
Provideripv64: providerGenerator(ipv64.NewDefaultConfig, ipv64.NewDNSProviderConfig),
|
|
||||||
Provideriwantmyname: providerGenerator(iwantmyname.NewDefaultConfig, iwantmyname.NewDNSProviderConfig),
|
|
||||||
Providerjoker: providerGenerator(joker.NewDefaultConfig, joker.NewDNSProviderConfig),
|
|
||||||
Providerliara: providerGenerator(liara.NewDefaultConfig, liara.NewDNSProviderConfig),
|
|
||||||
Providerlightsail: providerGenerator(lightsail.NewDefaultConfig, lightsail.NewDNSProviderConfig),
|
|
||||||
Providerlimacity: providerGenerator(limacity.NewDefaultConfig, limacity.NewDNSProviderConfig),
|
|
||||||
Providerlinode: providerGenerator(linode.NewDefaultConfig, linode.NewDNSProviderConfig),
|
|
||||||
Providerliquidweb: providerGenerator(liquidweb.NewDefaultConfig, liquidweb.NewDNSProviderConfig),
|
|
||||||
Providerloopia: providerGenerator(loopia.NewDefaultConfig, loopia.NewDNSProviderConfig),
|
|
||||||
Providerluadns: providerGenerator(luadns.NewDefaultConfig, luadns.NewDNSProviderConfig),
|
|
||||||
Providermailinabox: providerGenerator(mailinabox.NewDefaultConfig, mailinabox.NewDNSProviderConfig),
|
|
||||||
Providermanageengine: providerGenerator(manageengine.NewDefaultConfig, manageengine.NewDNSProviderConfig),
|
|
||||||
Providermetaname: providerGenerator(metaname.NewDefaultConfig, metaname.NewDNSProviderConfig),
|
|
||||||
Providermetaregistrar: providerGenerator(metaregistrar.NewDefaultConfig, metaregistrar.NewDNSProviderConfig),
|
|
||||||
Providermijnhost: providerGenerator(mijnhost.NewDefaultConfig, mijnhost.NewDNSProviderConfig),
|
|
||||||
Providermittwald: providerGenerator(mittwald.NewDefaultConfig, mittwald.NewDNSProviderConfig),
|
|
||||||
Providermyaddr: providerGenerator(myaddr.NewDefaultConfig, myaddr.NewDNSProviderConfig),
|
|
||||||
Providermydnsjp: providerGenerator(mydnsjp.NewDefaultConfig, mydnsjp.NewDNSProviderConfig),
|
|
||||||
Providernamecheap: providerGenerator(namecheap.NewDefaultConfig, namecheap.NewDNSProviderConfig),
|
|
||||||
Providernamedotcom: providerGenerator(namedotcom.NewDefaultConfig, namedotcom.NewDNSProviderConfig),
|
|
||||||
Providernamesilo: providerGenerator(namesilo.NewDefaultConfig, namesilo.NewDNSProviderConfig),
|
|
||||||
Providernearlyfreespeech: providerGenerator(nearlyfreespeech.NewDefaultConfig, nearlyfreespeech.NewDNSProviderConfig),
|
|
||||||
Providernetcup: providerGenerator(netcup.NewDefaultConfig, netcup.NewDNSProviderConfig),
|
|
||||||
Providernetlify: providerGenerator(netlify.NewDefaultConfig, netlify.NewDNSProviderConfig),
|
|
||||||
Providernicmanager: providerGenerator(nicmanager.NewDefaultConfig, nicmanager.NewDNSProviderConfig),
|
|
||||||
Providernifcloud: providerGenerator(nifcloud.NewDefaultConfig, nifcloud.NewDNSProviderConfig),
|
|
||||||
Providernjalla: providerGenerator(njalla.NewDefaultConfig, njalla.NewDNSProviderConfig),
|
|
||||||
Providernodion: providerGenerator(nodion.NewDefaultConfig, nodion.NewDNSProviderConfig),
|
|
||||||
Providerns1: providerGenerator(ns1.NewDefaultConfig, ns1.NewDNSProviderConfig),
|
|
||||||
Provideroraclecloud: providerGenerator(oraclecloud.NewDefaultConfig, oraclecloud.NewDNSProviderConfig),
|
|
||||||
Providerotc: providerGenerator(otc.NewDefaultConfig, otc.NewDNSProviderConfig),
|
|
||||||
Providerovh: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
|
|
||||||
Providerpdns: providerGenerator(pdns.NewDefaultConfig, pdns.NewDNSProviderConfig),
|
|
||||||
Providerplesk: providerGenerator(plesk.NewDefaultConfig, plesk.NewDNSProviderConfig),
|
|
||||||
Providerporkbun: providerGenerator(porkbun.NewDefaultConfig, porkbun.NewDNSProviderConfig),
|
|
||||||
Providerrackspace: providerGenerator(rackspace.NewDefaultConfig, rackspace.NewDNSProviderConfig),
|
|
||||||
Providerrainyun: providerGenerator(rainyun.NewDefaultConfig, rainyun.NewDNSProviderConfig),
|
|
||||||
Providerrcodezero: providerGenerator(rcodezero.NewDefaultConfig, rcodezero.NewDNSProviderConfig),
|
|
||||||
Providerregfish: providerGenerator(regfish.NewDefaultConfig, regfish.NewDNSProviderConfig),
|
|
||||||
Providerregru: providerGenerator(regru.NewDefaultConfig, regru.NewDNSProviderConfig),
|
|
||||||
Providerrfc2136: providerGenerator(rfc2136.NewDefaultConfig, rfc2136.NewDNSProviderConfig),
|
|
||||||
Providerrimuhosting: providerGenerator(rimuhosting.NewDefaultConfig, rimuhosting.NewDNSProviderConfig),
|
|
||||||
Providerroute53: providerGenerator(route53.NewDefaultConfig, route53.NewDNSProviderConfig),
|
|
||||||
Providersafedns: providerGenerator(safedns.NewDefaultConfig, safedns.NewDNSProviderConfig),
|
|
||||||
Providersakuracloud: providerGenerator(sakuracloud.NewDefaultConfig, sakuracloud.NewDNSProviderConfig),
|
|
||||||
Providerscaleway: providerGenerator(scaleway.NewDefaultConfig, scaleway.NewDNSProviderConfig),
|
|
||||||
Providerselectel: providerGenerator(selectel.NewDefaultConfig, selectel.NewDNSProviderConfig),
|
|
||||||
Providerselectelv2: providerGenerator(selectelv2.NewDefaultConfig, selectelv2.NewDNSProviderConfig),
|
|
||||||
Providerselfhostde: providerGenerator(selfhostde.NewDefaultConfig, selfhostde.NewDNSProviderConfig),
|
|
||||||
Providerservercow: providerGenerator(servercow.NewDefaultConfig, servercow.NewDNSProviderConfig),
|
|
||||||
Providershellrent: providerGenerator(shellrent.NewDefaultConfig, shellrent.NewDNSProviderConfig),
|
|
||||||
Providersimply: providerGenerator(simply.NewDefaultConfig, simply.NewDNSProviderConfig),
|
|
||||||
Providersonic: providerGenerator(sonic.NewDefaultConfig, sonic.NewDNSProviderConfig),
|
|
||||||
Providerspaceship: providerGenerator(spaceship.NewDefaultConfig, spaceship.NewDNSProviderConfig),
|
|
||||||
Providerstackpath: providerGenerator(stackpath.NewDefaultConfig, stackpath.NewDNSProviderConfig),
|
|
||||||
Providertechnitium: providerGenerator(technitium.NewDefaultConfig, technitium.NewDNSProviderConfig),
|
|
||||||
Providertencentcloud: providerGenerator(tencentcloud.NewDefaultConfig, tencentcloud.NewDNSProviderConfig),
|
|
||||||
Providertimewebcloud: providerGenerator(timewebcloud.NewDefaultConfig, timewebcloud.NewDNSProviderConfig),
|
|
||||||
Providertransip: providerGenerator(transip.NewDefaultConfig, transip.NewDNSProviderConfig),
|
|
||||||
Providerultradns: providerGenerator(ultradns.NewDefaultConfig, ultradns.NewDNSProviderConfig),
|
|
||||||
Providervariomedia: providerGenerator(variomedia.NewDefaultConfig, variomedia.NewDNSProviderConfig),
|
|
||||||
Providervegadns: providerGenerator(vegadns.NewDefaultConfig, vegadns.NewDNSProviderConfig),
|
|
||||||
Providervercel: providerGenerator(vercel.NewDefaultConfig, vercel.NewDNSProviderConfig),
|
|
||||||
Providerversio: providerGenerator(versio.NewDefaultConfig, versio.NewDNSProviderConfig),
|
|
||||||
Providervinyldns: providerGenerator(vinyldns.NewDefaultConfig, vinyldns.NewDNSProviderConfig),
|
|
||||||
Providervkcloud: providerGenerator(vkcloud.NewDefaultConfig, vkcloud.NewDNSProviderConfig),
|
|
||||||
Providervolcengine: providerGenerator(volcengine.NewDefaultConfig, volcengine.NewDNSProviderConfig),
|
|
||||||
Providervscale: providerGenerator(vscale.NewDefaultConfig, vscale.NewDNSProviderConfig),
|
|
||||||
Providervultr: providerGenerator(vultr.NewDefaultConfig, vultr.NewDNSProviderConfig),
|
|
||||||
Providerwebnames: providerGenerator(webnames.NewDefaultConfig, webnames.NewDNSProviderConfig),
|
|
||||||
Providerwebsupport: providerGenerator(websupport.NewDefaultConfig, websupport.NewDNSProviderConfig),
|
|
||||||
Providerwedos: providerGenerator(wedos.NewDefaultConfig, wedos.NewDNSProviderConfig),
|
|
||||||
Providerwestcn: providerGenerator(westcn.NewDefaultConfig, westcn.NewDNSProviderConfig),
|
|
||||||
Provideryandex: providerGenerator(yandex.NewDefaultConfig, yandex.NewDNSProviderConfig),
|
|
||||||
Provideryandex360: providerGenerator(yandex360.NewDefaultConfig, yandex360.NewDNSProviderConfig),
|
|
||||||
Providerzoneee: providerGenerator(zoneee.NewDefaultConfig, zoneee.NewDNSProviderConfig),
|
|
||||||
Providerzonomi: providerGenerator(zonomi.NewDefaultConfig, zonomi.NewDNSProviderConfig),
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,14 +13,14 @@ func (p *Provider) Setup() (err error) {
|
||||||
if !errors.Is(err, os.ErrNotExist) { // ignore if cert doesn't exist
|
if !errors.Is(err, os.ErrNotExist) { // ignore if cert doesn't exist
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logging.Debug().Msg("obtaining cert due to error loading cert")
|
log.Debug().Msg("obtaining cert due to error loading cert")
|
||||||
if err = p.ObtainCert(); err != nil {
|
if err = p.ObtainCert(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, expiry := range p.GetExpiries() {
|
for _, expiry := range p.GetExpiries() {
|
||||||
logging.Info().Msg("certificate expire on " + strutils.FormatTime(expiry))
|
log.Info().Msg("certificate expire on " + strutils.FormatTime(expiry))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,8 @@ const (
|
||||||
ConfigExampleFileName = "config.example.yml"
|
ConfigExampleFileName = "config.example.yml"
|
||||||
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
||||||
|
|
||||||
IconListCachePath = ConfigBasePath + "/.icon_list_cache.json"
|
DataDir = "data"
|
||||||
IconCachePath = ConfigBasePath + "/.icon_cache.json"
|
IconListCachePath = DataDir + "/.icon_list_cache.json"
|
||||||
|
|
||||||
NamespaceHomepageOverrides = ".homepage"
|
NamespaceHomepageOverrides = ".homepage"
|
||||||
NamespaceIconCache = ".icon_cache"
|
NamespaceIconCache = ".icon_cache"
|
||||||
|
@ -25,16 +25,12 @@ const (
|
||||||
|
|
||||||
ComposeFileName = "compose.yml"
|
ComposeFileName = "compose.yml"
|
||||||
ComposeExampleFileName = "compose.example.yml"
|
ComposeExampleFileName = "compose.example.yml"
|
||||||
|
|
||||||
DataDir = "data"
|
|
||||||
|
|
||||||
ErrorPagesBasePath = "error_pages"
|
ErrorPagesBasePath = "error_pages"
|
||||||
|
|
||||||
AgentCertsBasePath = "certs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var RequiredDirectories = []string{
|
var RequiredDirectories = []string{
|
||||||
ConfigBasePath,
|
ConfigBasePath,
|
||||||
|
DataDir,
|
||||||
ErrorPagesBasePath,
|
ErrorPagesBasePath,
|
||||||
MiddlewareComposeBasePath,
|
MiddlewareComposeBasePath,
|
||||||
}
|
}
|
||||||
|
@ -44,6 +40,7 @@ const DockerHostFromEnv = "$DOCKER_HOST"
|
||||||
const (
|
const (
|
||||||
HealthCheckIntervalDefault = 5 * time.Second
|
HealthCheckIntervalDefault = 5 * time.Second
|
||||||
HealthCheckTimeoutDefault = 5 * time.Second
|
HealthCheckTimeoutDefault = 5 * time.Second
|
||||||
|
HealthCheckDownNotifyDelayDefault = 15 * time.Second
|
||||||
|
|
||||||
WakeTimeoutDefault = "3m"
|
WakeTimeoutDefault = "3m"
|
||||||
StopTimeoutDefault = "3m"
|
StopTimeoutDefault = "3m"
|
||||||
|
|
|
@ -3,8 +3,7 @@ package common
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"log"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func decodeJWTKey(key string) []byte {
|
func decodeJWTKey(key string) []byte {
|
||||||
|
@ -13,7 +12,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.Fatal().Str("key", key).Err(err).Msg("failed to decode secret")
|
log.Fatalf("failed to decode secret: %s", err)
|
||||||
}
|
}
|
||||||
return bytes
|
return bytes
|
||||||
}
|
}
|
||||||
|
@ -22,7 +21,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.Fatal().Err(err).Msg("failed to generate random jwt key")
|
log.Fatalf("failed to generate random jwt key: %s", err)
|
||||||
}
|
}
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,13 @@ package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -48,7 +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", "")
|
||||||
OIDCScopes = GetEnvString("OIDC_SCOPES", "openid, profile, email")
|
OIDCScopes = GetCommaSepEnv("OIDC_SCOPES", "openid, profile, email, groups")
|
||||||
OIDCAllowedUsers = GetCommaSepEnv("OIDC_ALLOWED_USERS", "")
|
OIDCAllowedUsers = GetCommaSepEnv("OIDC_ALLOWED_USERS", "")
|
||||||
OIDCAllowedGroups = GetCommaSepEnv("OIDC_ALLOWED_GROUPS", "")
|
OIDCAllowedGroups = GetCommaSepEnv("OIDC_ALLOWED_GROUPS", "")
|
||||||
|
|
||||||
|
@ -58,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 {
|
||||||
|
@ -76,14 +78,16 @@ func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
log.Fatal().Err(err).Msgf("env %s: invalid %T value: %s", key, parsed, value)
|
log.Fatalf("env %s: invalid %T value: %s", key, parsed, value)
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetEnvString(key string, defaultValue string) string {
|
func stringstring(s string) (string, error) {
|
||||||
return GetEnv(key, defaultValue, func(s string) (string, error) {
|
|
||||||
return s, nil
|
return s, nil
|
||||||
})
|
}
|
||||||
|
|
||||||
|
func GetEnvString(key string, defaultValue string) string {
|
||||||
|
return GetEnv(key, defaultValue, stringstring)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetEnvBool(key string, defaultValue bool) bool {
|
func GetEnvBool(key string, defaultValue bool) bool {
|
||||||
|
@ -101,7 +105,7 @@ func GetAddrEnv(key, defaultValue, scheme string) (addr, host string, portInt in
|
||||||
}
|
}
|
||||||
host, port, err := net.SplitHostPort(addr)
|
host, port, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Msgf("env %s: invalid address: %s", key, addr)
|
log.Fatalf("env %s: invalid address: %s", key, addr)
|
||||||
}
|
}
|
||||||
if host == "" {
|
if host == "" {
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
|
@ -109,7 +113,7 @@ func GetAddrEnv(key, defaultValue, scheme string) (addr, host string, portInt in
|
||||||
fullURL = fmt.Sprintf("%s://%s:%s", scheme, host, port)
|
fullURL = fmt.Sprintf("%s://%s:%s", scheme, host, port)
|
||||||
portInt, err = strconv.Atoi(port)
|
portInt, err = strconv.Atoi(port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Msgf("env %s: invalid port: %s", key, port)
|
log.Fatalf("env %s: invalid port: %s", key, port)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
"github.com/yusing/go-proxy/internal/route/provider"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/functional"
|
|
||||||
)
|
|
||||||
|
|
||||||
var agentPool = functional.NewMapOf[string, *agent.AgentConfig]()
|
|
||||||
|
|
||||||
func addAgent(agent *agent.AgentConfig) {
|
|
||||||
agentPool.Store(agent.Addr, agent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeAllAgents() {
|
|
||||||
agentPool.Clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAgent(addr string) (agent *agent.AgentConfig, ok bool) {
|
|
||||||
agent, ok = agentPool.Load(addr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) GetAgent(agentAddrOrDockerHost string) (*agent.AgentConfig, bool) {
|
|
||||||
if !agent.IsDockerHostAgent(agentAddrOrDockerHost) {
|
|
||||||
return GetAgent(agentAddrOrDockerHost)
|
|
||||||
}
|
|
||||||
return GetAgent(agent.GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error) {
|
|
||||||
if slices.ContainsFunc(cfg.value.Providers.Agents, func(a *agent.AgentConfig) bool {
|
|
||||||
return a.Addr == host
|
|
||||||
}) {
|
|
||||||
return 0, gperr.New("agent already exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
var agentCfg agent.AgentConfig
|
|
||||||
agentCfg.Addr = host
|
|
||||||
err := agentCfg.StartWithCerts(cfg.Task(), ca.Cert, client.Cert, client.Key)
|
|
||||||
if err != nil {
|
|
||||||
return 0, gperr.Wrap(err, "failed to start agent")
|
|
||||||
}
|
|
||||||
addAgent(&agentCfg)
|
|
||||||
|
|
||||||
provider := provider.NewAgentProvider(&agentCfg)
|
|
||||||
if err := cfg.errIfExists(provider); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
err = provider.LoadRoutes()
|
|
||||||
if err != nil {
|
|
||||||
return 0, gperr.Wrap(err, "failed to load routes")
|
|
||||||
}
|
|
||||||
return provider.NumRoutes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) ListAgents() []*agent.AgentConfig {
|
|
||||||
agents := make([]*agent.AgentConfig, 0, agentPool.Size())
|
|
||||||
agentPool.RangeAll(func(key string, value *agent.AgentConfig) {
|
|
||||||
agents = append(agents, value)
|
|
||||||
})
|
|
||||||
return agents
|
|
||||||
}
|
|
37
internal/config/agents.go
Normal file
37
internal/config/agents.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/route/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error) {
|
||||||
|
if slices.ContainsFunc(cfg.value.Providers.Agents, func(a *agent.AgentConfig) bool {
|
||||||
|
return a.Addr == host
|
||||||
|
}) {
|
||||||
|
return 0, gperr.New("agent already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
var agentCfg agent.AgentConfig
|
||||||
|
agentCfg.Addr = host
|
||||||
|
err := agentCfg.StartWithCerts(cfg.Task().Context(), ca.Cert, client.Cert, client.Key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, gperr.Wrap(err, "failed to start agent")
|
||||||
|
}
|
||||||
|
agent.AddAgent(&agentCfg)
|
||||||
|
|
||||||
|
provider := provider.NewAgentProvider(&agentCfg)
|
||||||
|
if err := cfg.errIfExists(provider); err != nil {
|
||||||
|
agent.RemoveAgent(&agentCfg)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
err = provider.LoadRoutes()
|
||||||
|
if err != nil {
|
||||||
|
agent.RemoveAgent(&agentCfg)
|
||||||
|
return 0, gperr.Wrap(err, "failed to load routes")
|
||||||
|
}
|
||||||
|
return provider.NumRoutes(), nil
|
||||||
|
}
|
|
@ -9,19 +9,24 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
agentPkg "github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
"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"
|
||||||
"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/maxmind"
|
||||||
"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/serialization"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
|
||||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||||
"github.com/yusing/go-proxy/internal/watcher"
|
"github.com/yusing/go-proxy/internal/watcher"
|
||||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||||
)
|
)
|
||||||
|
@ -92,10 +97,10 @@ func OnConfigChange(ev []events.Event) {
|
||||||
// just reload once and check the last event
|
// just reload once and check the last event
|
||||||
switch ev[len(ev)-1].Action {
|
switch ev[len(ev)-1].Action {
|
||||||
case events.ActionFileRenamed:
|
case events.ActionFileRenamed:
|
||||||
logging.Warn().Msg(cfgRenameWarn)
|
log.Warn().Msg(cfgRenameWarn)
|
||||||
return
|
return
|
||||||
case events.ActionFileDeleted:
|
case events.ActionFileDeleted:
|
||||||
logging.Warn().Msg(cfgDeleteWarn)
|
log.Warn().Msg(cfgDeleteWarn)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,13 +118,13 @@ func Reload() gperr.Error {
|
||||||
newCfg := newConfig()
|
newCfg := newConfig()
|
||||||
err := newCfg.load()
|
err := newCfg.load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
newCfg.task.Finish(err)
|
newCfg.task.FinishAndWait(err)
|
||||||
return gperr.New("using last config").With(err)
|
return gperr.New(ansi.Warning("using last config")).With(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cancel all current subtasks -> wait
|
// cancel all current subtasks -> wait
|
||||||
// -> replace config -> start new subtasks
|
// -> replace config -> start new subtasks
|
||||||
config.GetInstance().(*Config).Task().Finish("config changed")
|
config.GetInstance().(*Config).Task().FinishAndWait("config changed")
|
||||||
newCfg.Start(StartAllServers)
|
newCfg.Start(StartAllServers)
|
||||||
config.SetInstance(newCfg)
|
config.SetInstance(newCfg)
|
||||||
return nil
|
return nil
|
||||||
|
@ -157,7 +162,7 @@ func (cfg *Config) Start(opts ...*StartServersOptions) {
|
||||||
func (cfg *Config) StartAutoCert() {
|
func (cfg *Config) StartAutoCert() {
|
||||||
autocert := cfg.autocertProvider
|
autocert := cfg.autocertProvider
|
||||||
if autocert == nil {
|
if autocert == nil {
|
||||||
logging.Info().Msg("autocert not configured")
|
log.Info().Msg("autocert not configured")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +224,7 @@ func (cfg *Config) load() gperr.Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
model := config.DefaultConfig()
|
model := config.DefaultConfig()
|
||||||
if err := utils.DeserializeYAML(data, model); err != nil {
|
if err := serialization.UnmarshalValidateYAML(data, model); err != nil {
|
||||||
gperr.LogFatal(errMsg, err)
|
gperr.LogFatal(errMsg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,8 +232,10 @@ func (cfg *Config) load() gperr.Error {
|
||||||
errs := gperr.NewBuilder(errMsg)
|
errs := gperr.NewBuilder(errMsg)
|
||||||
errs.Add(cfg.entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
|
errs.Add(cfg.entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
|
||||||
errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
|
errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
|
||||||
|
errs.Add(cfg.initMaxMind(model.Providers.MaxMind))
|
||||||
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
|
||||||
|
@ -242,13 +249,26 @@ func (cfg *Config) load() gperr.Error {
|
||||||
err := model.ACL.Start(cfg.task)
|
err := model.ACL.Start(cfg.task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Add(err)
|
errs.Add(err)
|
||||||
} else {
|
|
||||||
logging.Info().Msg("ACL started")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errs.HasError() {
|
||||||
|
notif.Notify(¬if.LogMessage{
|
||||||
|
Level: zerolog.ErrorLevel,
|
||||||
|
Title: "Config Reload Error",
|
||||||
|
Body: notif.ErrorBody{Error: errs.Error()},
|
||||||
|
})
|
||||||
return errs.Error()
|
return errs.Error()
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) initMaxMind(maxmindCfg *maxmind.Config) gperr.Error {
|
||||||
|
if maxmindCfg != nil {
|
||||||
|
return maxmind.SetInstance(cfg.task, maxmindCfg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (cfg *Config) initNotification(notifCfg []notif.NotificationConfig) {
|
func (cfg *Config) initNotification(notifCfg []notif.NotificationConfig) {
|
||||||
if len(notifCfg) == 0 {
|
if len(notifCfg) == 0 {
|
||||||
|
@ -260,13 +280,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()
|
||||||
|
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 {
|
||||||
|
@ -284,14 +324,14 @@ func (cfg *Config) loadRouteProviders(providers *config.Providers) gperr.Error {
|
||||||
errs := gperr.NewBuilder("route provider errors")
|
errs := gperr.NewBuilder("route provider errors")
|
||||||
results := gperr.NewBuilder("loaded route providers")
|
results := gperr.NewBuilder("loaded route providers")
|
||||||
|
|
||||||
removeAllAgents()
|
agentPkg.RemoveAllAgents()
|
||||||
|
|
||||||
for _, agent := range providers.Agents {
|
for _, agent := range providers.Agents {
|
||||||
if err := agent.Start(cfg.task); err != nil {
|
if err := agent.Start(cfg.task.Context()); err != nil {
|
||||||
errs.Add(err.Subject(agent.String()))
|
errs.Add(gperr.PrependSubject(agent.String(), err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
addAgent(agent)
|
agentPkg.AddAgent(agent)
|
||||||
p := proxy.NewAgentProvider(agent)
|
p := proxy.NewAgentProvider(agent)
|
||||||
if err := cfg.errIfExists(p); err != nil {
|
if err := cfg.errIfExists(p); err != nil {
|
||||||
errs.Add(err.Subject(p.String()))
|
errs.Add(err.Subject(p.String()))
|
||||||
|
@ -335,6 +375,6 @@ func (cfg *Config) loadRouteProviders(providers *config.Providers) gperr.Error {
|
||||||
}
|
}
|
||||||
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
|
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
|
||||||
})
|
})
|
||||||
logging.Info().Msg(results.String())
|
log.Info().Msg(results.String())
|
||||||
return errs.Error()
|
return errs.Error()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,26 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/yusing/go-proxy/internal/route"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
"github.com/yusing/go-proxy/internal/route/provider"
|
"github.com/yusing/go-proxy/internal/route/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (cfg *Config) DumpRoutes() map[string]*route.Route {
|
|
||||||
entries := make(map[string]*route.Route)
|
|
||||||
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
|
||||||
p.RangeRoutes(func(alias string, r *route.Route) {
|
|
||||||
entries[alias] = r
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return entries
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) DumpRouteProviders() map[string]*provider.Provider {
|
func (cfg *Config) DumpRouteProviders() map[string]*provider.Provider {
|
||||||
entries := make(map[string]*provider.Provider)
|
entries := make(map[string]*provider.Provider, cfg.providers.Size())
|
||||||
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
for _, p := range cfg.providers.Range {
|
||||||
entries[p.ShortName()] = p
|
entries[p.ShortName()] = p
|
||||||
})
|
}
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) RouteProviderList() []string {
|
func (cfg *Config) RouteProviderList() []config.RouteProviderListResponse {
|
||||||
var list []string
|
list := make([]config.RouteProviderListResponse, 0, cfg.providers.Size())
|
||||||
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
for _, p := range cfg.providers.Range {
|
||||||
list = append(list, p.ShortName())
|
list = append(list, config.RouteProviderListResponse{
|
||||||
|
ShortName: p.ShortName(),
|
||||||
|
FullName: p.String(),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,13 +29,13 @@ func (cfg *Config) Statistics() map[string]any {
|
||||||
var total uint16
|
var total uint16
|
||||||
providerStats := make(map[string]provider.ProviderStats)
|
providerStats := make(map[string]provider.ProviderStats)
|
||||||
|
|
||||||
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
for _, p := range cfg.providers.Range {
|
||||||
stats := p.Statistics()
|
stats := p.Statistics()
|
||||||
providerStats[p.ShortName()] = stats
|
providerStats[p.ShortName()] = stats
|
||||||
rps.AddOther(stats.RPs)
|
rps.AddOther(stats.RPs)
|
||||||
streams.AddOther(stats.Streams)
|
streams.AddOther(stats.Streams)
|
||||||
total += stats.RPs.Total + stats.Streams.Total
|
total += stats.RPs.Total + stats.Streams.Total
|
||||||
})
|
}
|
||||||
|
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"total": total,
|
"total": total,
|
||||||
|
|
|
@ -11,14 +11,16 @@ import (
|
||||||
"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/logging/accesslog"
|
"github.com/yusing/go-proxy/internal/logging/accesslog"
|
||||||
|
maxmind "github.com/yusing/go-proxy/internal/maxmind/types"
|
||||||
"github.com/yusing/go-proxy/internal/notif"
|
"github.com/yusing/go-proxy/internal/notif"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/proxmox"
|
||||||
|
"github.com/yusing/go-proxy/internal/serialization"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Config struct {
|
Config struct {
|
||||||
ACL *acl.Config `json:"acl"`
|
ACL *acl.Config `json:"acl"`
|
||||||
AutoCert *autocert.AutocertConfig `json:"autocert"`
|
AutoCert *autocert.Config `json:"autocert"`
|
||||||
Entrypoint Entrypoint `json:"entrypoint"`
|
Entrypoint Entrypoint `json:"entrypoint"`
|
||||||
Providers Providers `json:"providers"`
|
Providers Providers `json:"providers"`
|
||||||
MatchDomains []string `json:"match_domains" validate:"domain_name"`
|
MatchDomains []string `json:"match_domains" validate:"domain_name"`
|
||||||
|
@ -30,6 +32,8 @@ type (
|
||||||
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"`
|
||||||
|
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
|
||||||
}
|
}
|
||||||
Entrypoint struct {
|
Entrypoint struct {
|
||||||
Middlewares []map[string]any `json:"middlewares"`
|
Middlewares []map[string]any `json:"middlewares"`
|
||||||
|
@ -38,16 +42,17 @@ type (
|
||||||
HomepageConfig struct {
|
HomepageConfig struct {
|
||||||
UseDefaultCategories bool `json:"use_default_categories"`
|
UseDefaultCategories bool `json:"use_default_categories"`
|
||||||
}
|
}
|
||||||
|
RouteProviderListResponse struct {
|
||||||
|
ShortName string `json:"short_name"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
}
|
||||||
ConfigInstance interface {
|
ConfigInstance interface {
|
||||||
Value() *Config
|
Value() *Config
|
||||||
Reload() gperr.Error
|
Reload() gperr.Error
|
||||||
Statistics() map[string]any
|
Statistics() map[string]any
|
||||||
RouteProviderList() []string
|
RouteProviderList() []RouteProviderListResponse
|
||||||
Context() context.Context
|
Context() context.Context
|
||||||
GetAgent(agentAddrOrDockerHost string) (*agent.AgentConfig, bool)
|
|
||||||
VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error)
|
VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error)
|
||||||
ListAgents() []*agent.AgentConfig
|
|
||||||
AutoCertProvider() *autocert.Provider
|
AutoCertProvider() *autocert.Provider
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -86,14 +91,14 @@ func HasInstance() bool {
|
||||||
|
|
||||||
func Validate(data []byte) gperr.Error {
|
func Validate(data []byte) gperr.Error {
|
||||||
var model Config
|
var model Config
|
||||||
return utils.DeserializeYAML(data, &model)
|
return serialization.UnmarshalValidateYAML(data, &model)
|
||||||
}
|
}
|
||||||
|
|
||||||
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)
|
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
utils.RegisterDefaultValueFactory(DefaultConfig)
|
serialization.RegisterDefaultValueFactory(DefaultConfig)
|
||||||
utils.MustRegisterValidation("domain_name", func(fl validator.FieldLevel) bool {
|
serialization.MustRegisterValidation("domain_name", func(fl validator.FieldLevel) bool {
|
||||||
domains := fl.Field().Interface().([]string)
|
domains := fl.Field().Interface().([]string)
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
if !matchDomainsRegex.MatchString(domain) {
|
if !matchDomainsRegex.MatchString(domain) {
|
||||||
|
@ -102,7 +107,7 @@ func init() {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
utils.MustRegisterValidation("non_empty_docker_keys", func(fl validator.FieldLevel) bool {
|
serialization.MustRegisterValidation("non_empty_docker_keys", func(fl validator.FieldLevel) bool {
|
||||||
m := fl.Field().Interface().(map[string]string)
|
m := fl.Field().Interface().(map[string]string)
|
||||||
for k := range m {
|
for k := range m {
|
||||||
if k == "" {
|
if k == "" {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package autocert
|
package dnsproviders
|
||||||
|
|
||||||
type DummyConfig struct{}
|
type DummyConfig struct{}
|
||||||
type DummyProvider struct{}
|
type DummyProvider struct{}
|
|
@ -10,15 +10,17 @@ url = "https://api.github.com/repos/go-acme/lego/contents/providers/dns"
|
||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
data: list[Entry] = [Entry(**i) for i in response.json()]
|
data: list[Entry] = [Entry(**i) for i in response.json()]
|
||||||
|
|
||||||
header = "//go:generate /usr/bin/python3 gen.py\n\npackage autocert\n\n"
|
header = "//go:generate /usr/bin/python3 gen.py\n\npackage dnsproviders\n\n"
|
||||||
names: list[str] = [
|
names: list[str] = [
|
||||||
"ProviderLocal = \"local\"",
|
"Local = \"local\"",
|
||||||
"ProviderPseudo = \"pseudo\"",
|
"Pseudo = \"pseudo\"",
|
||||||
|
]
|
||||||
|
imports: list[str] = [
|
||||||
|
"\"github.com/yusing/go-proxy/internal/autocert\""
|
||||||
]
|
]
|
||||||
imports: list[str] = []
|
|
||||||
genMap: list[str] = [
|
genMap: list[str] = [
|
||||||
"ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),",
|
"autocert.Providers[Local] = autocert.DNSProvider(NewDummyDefaultConfig, NewDummyDNSProviderConfig)",
|
||||||
"ProviderPseudo: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),",
|
"autocert.Providers[Pseudo] = autocert.DNSProvider(NewDummyDefaultConfig, NewDummyDNSProviderConfig)",
|
||||||
]
|
]
|
||||||
|
|
||||||
blacklists = [
|
blacklists = [
|
||||||
|
@ -35,18 +37,18 @@ blacklists = [
|
||||||
for item in data:
|
for item in data:
|
||||||
if item.type != "dir" or item.name in blacklists:
|
if item.type != "dir" or item.name in blacklists:
|
||||||
continue
|
continue
|
||||||
imports.append(f"import \"github.com/go-acme/lego/v4/providers/dns/{item.name}\"")
|
imports.append(f"\"github.com/go-acme/lego/v4/providers/dns/{item.name}\"")
|
||||||
names.append(f"Provider{item.name} = \"{item.name}\"")
|
genMap.append(f"autocert.Providers[\"{item.name}\"] = autocert.DNSProvider({item.name}.NewDefaultConfig, {item.name}.NewDNSProviderConfig)")
|
||||||
genMap.append(f"Provider{item.name}: providerGenerator({item.name}.NewDefaultConfig, {item.name}.NewDNSProviderConfig),")
|
|
||||||
|
|
||||||
with open("providers.go", "w") as f:
|
with open("providers.go", "w") as f:
|
||||||
f.write(header)
|
f.write(header)
|
||||||
|
f.write("import (\n")
|
||||||
f.write("\n".join(imports))
|
f.write("\n".join(imports))
|
||||||
f.write("\n\n")
|
f.write("\n)\n\n")
|
||||||
f.write("const (\n")
|
f.write("const (\n")
|
||||||
f.write("\n".join(names))
|
f.write("\n".join(names))
|
||||||
f.write("\n)\n\n")
|
f.write("\n)\n\n")
|
||||||
f.write("var providers = map[string]ProviderGenerator{\n")
|
f.write("func InitProviders() {\n")
|
||||||
f.write("\n".join(genMap))
|
f.write("\n".join(genMap))
|
||||||
f.write("\n}\n\n")
|
f.write("\n}\n\n")
|
||||||
|
|
185
internal/dnsproviders/go.mod
Normal file
185
internal/dnsproviders/go.mod
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
module github.com/yusing/go-proxy/internal/dnsproviders
|
||||||
|
|
||||||
|
go 1.24.5
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy => ../..
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy/internal/utils => ../utils
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-acme/lego/v4 v4.25.1
|
||||||
|
github.com/yusing/go-proxy v0.16.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/auth v0.16.3 // indirect
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||||
|
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // 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/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||||
|
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.9 // indirect
|
||||||
|
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||||
|
github.com/alibabacloud-go/tea v1.3.10 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||||
|
github.com/aliyun/credentials-go v1.4.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.36.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.29.18 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.71 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // 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.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 // indirect
|
||||||
|
github.com/aws/smithy-go v1.22.5 // indirect
|
||||||
|
github.com/baidubce/bce-sdk-go v0.9.236 // indirect
|
||||||
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||||
|
github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
|
||||||
|
github.com/exoscale/egoscale/v3 v3.1.24 // 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/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
|
github.com/go-acme/alidns-20150109/v4 v4.5.11 // indirect
|
||||||
|
github.com/go-acme/tencentclouddnspod v1.0.1208 // indirect
|
||||||
|
github.com/go-errors/errors v1.5.1 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // 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.27.0 // indirect
|
||||||
|
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
|
github.com/gofrs/flock v0.12.1 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.3 // 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.15.0 // indirect
|
||||||
|
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
||||||
|
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
|
||||||
|
github.com/gotify/server/v2 v2.6.3 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||||
|
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||||
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.160 // indirect
|
||||||
|
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||||
|
github.com/infobloxopen/infoblox-go-client/v2 v2.10.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.53.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.67 // 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/v4 v4.0.2 // 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/oci-go-sdk/common/v1065 v1065.96.0 // indirect
|
||||||
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.96.0 // indirect
|
||||||
|
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||||
|
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||||
|
github.com/ovh/go-ovh v1.9.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/pquerna/otp v1.5.0 // indirect
|
||||||
|
github.com/puzpuzpuz/xsync/v4 v4.1.0 // 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.3.2 // indirect
|
||||||
|
github.com/sacloud/go-http v0.1.9 // indirect
|
||||||
|
github.com/sacloud/iaas-api-go v1.16.1 // 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.34 // indirect
|
||||||
|
github.com/selectel/domains-go v1.1.0 // indirect
|
||||||
|
github.com/selectel/go-selvpcclient/v4 v4.1.0 // 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.9.2 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.7 // 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.1214 // 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.1-20250722213956-faef419 // indirect
|
||||||
|
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||||
|
github.com/volcengine/volc-sdk-golang v1.0.216 // indirect
|
||||||
|
github.com/vultr/govultr/v3 v3.21.1 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
|
github.com/yusing/go-proxy/internal/utils v0.0.0 // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.17.4 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.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.40.0 // indirect
|
||||||
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
|
golang.org/x/net v0.42.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
|
golang.org/x/text v0.27.0 // indirect
|
||||||
|
golang.org/x/time v0.12.0 // indirect
|
||||||
|
golang.org/x/tools v0.35.0 // indirect
|
||||||
|
google.golang.org/api v0.243.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 // indirect
|
||||||
|
google.golang.org/grpc v1.74.2 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/ns1/ns1-go.v2 v2.14.4 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
2519
internal/dnsproviders/go.sum
Normal file
2519
internal/dnsproviders/go.sum
Normal file
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue