mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-20 20:52:33 +02:00
Compare commits
211 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
759995972d | ||
![]() |
03401488f6 | ||
![]() |
1e790be70c | ||
![]() |
4410637f8b | ||
![]() |
3947152336 | ||
![]() |
af8d2c74f6 | ||
![]() |
e107f8d476 | ||
![]() |
b427ff1f88 | ||
![]() |
e513db62b0 | ||
![]() |
2f33ee02d9 | ||
![]() |
59490dcac0 | ||
![]() |
5afa93a8f1 | ||
![]() |
c8e9ed8440 | ||
![]() |
8363dfe257 | ||
![]() |
080bbc18eb | ||
![]() |
1a0edc8bfe | ||
![]() |
e8d1d524b9 | ||
![]() |
edada22ac0 | ||
![]() |
76fb0cfdbb | ||
![]() |
5df2553774 | ||
![]() |
31812430f1 | ||
![]() |
d668b03175 | ||
![]() |
663a107c06 | ||
![]() |
806184e98b | ||
![]() |
08ee82d7b0 | ||
![]() |
bcc19167d4 | ||
![]() |
858f65ee5a | ||
![]() |
43566bbcfd | ||
![]() |
ec8cca1245 | ||
![]() |
4a65de99a8 | ||
![]() |
7461344004 | ||
![]() |
b815c6fd69 | ||
![]() |
28c9a2e9d0 | ||
![]() |
9e0bdd964c | ||
![]() |
077641beaa | ||
![]() |
ef483403da | ||
![]() |
0a8aa2b215 | ||
![]() |
5a984f5c0c | ||
![]() |
d60688c66f | ||
![]() |
23482da259 | ||
![]() |
62776229cb | ||
![]() |
36fab0cd50 | ||
![]() |
8f03662982 | ||
![]() |
aad44031c4 | ||
![]() |
51813e6030 |
405 changed files with 21503 additions and 9317 deletions
44
.env.example
44
.env.example
|
@ -1,24 +1,33 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# API JWT Configuration (common)
|
||||||
|
# generate secret with `openssl rand -base64 32`
|
||||||
|
GODOXY_API_JWT_SECRET=
|
||||||
|
# the JWT token time-to-live
|
||||||
|
# leave empty to use default (24 hours)
|
||||||
|
# format: https://pkg.go.dev/time#Duration
|
||||||
|
GODOXY_API_JWT_TOKEN_TTL=
|
||||||
|
|
||||||
# API/WebUI user password login credentials (optional)
|
# API/WebUI user password login credentials (optional)
|
||||||
# These fields are not required for OIDC authentication
|
# These fields are not required for OIDC authentication
|
||||||
GODOXY_API_USER=admin
|
GODOXY_API_USER=admin
|
||||||
GODOXY_API_PASSWORD=password
|
GODOXY_API_PASSWORD=password
|
||||||
# generate secret with `openssl rand -base64 32`
|
|
||||||
GODOXY_API_JWT_SECRET=
|
|
||||||
# the JWT token time-to-live
|
|
||||||
GODOXY_API_JWT_TOKEN_TTL=1h
|
|
||||||
|
|
||||||
# OIDC Configuration (optional)
|
# OIDC Configuration (optional)
|
||||||
# Uncomment and configure these values to enable OIDC authentication.
|
# Uncomment and configure these values to enable OIDC authentication.
|
||||||
|
#
|
||||||
# GODOXY_OIDC_ISSUER_URL=https://accounts.google.com
|
# GODOXY_OIDC_ISSUER_URL=https://accounts.google.com
|
||||||
# GODOXY_OIDC_CLIENT_ID=your-client-id
|
# GODOXY_OIDC_CLIENT_ID=your-client-id
|
||||||
# GODOXY_OIDC_CLIENT_SECRET=your-client-secret
|
# GODOXY_OIDC_CLIENT_SECRET=your-client-secret
|
||||||
# Keep /api/auth/callback as the redirect URL, change the domain to match your setup.
|
# GODOXY_OIDC_SCOPES=openid, profile, email, groups # you may also include `offline_access` if your Idp supports it (e.g. Authentik, Pocket ID)
|
||||||
# GODOXY_OIDC_REDIRECT_URL=https://your-domain/api/auth/callback
|
|
||||||
# Comma-separated list of scopes
|
|
||||||
# GODOXY_OIDC_SCOPES=openid, profile, email
|
|
||||||
#
|
#
|
||||||
# User definitions: Uncomment and configure these values to restrict access to specific users or groups.
|
# User definitions: Uncomment and configure these values to restrict access to specific users or groups.
|
||||||
# These two fields act as a logical AND operator. For example, given the following membership:
|
# These two fields act as a logical AND operator. For example, given the following membership:
|
||||||
|
@ -39,14 +48,29 @@ GODOXY_API_JWT_TOKEN_TTL=1h
|
||||||
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 }}
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -11,6 +11,7 @@ error_pages/
|
||||||
!examples/error_pages/
|
!examples/error_pages/
|
||||||
profiles/
|
profiles/
|
||||||
data/
|
data/
|
||||||
|
debug/
|
||||||
|
|
||||||
logs/
|
logs/
|
||||||
log/
|
log/
|
||||||
|
@ -28,6 +29,8 @@ todo.md
|
||||||
.aider*
|
.aider*
|
||||||
mtrace.json
|
mtrace.json
|
||||||
.env
|
.env
|
||||||
|
.cursorrules
|
||||||
|
.windsurfrules
|
||||||
test.Dockerfile
|
test.Dockerfile
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
204
.golangci.yml
204
.golangci.yml
|
@ -1,43 +1,77 @@
|
||||||
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
|
||||||
|
- forcetypeassert
|
||||||
|
- gochecknoglobals
|
||||||
|
- gochecknoinits
|
||||||
|
- gocognit
|
||||||
|
- goconst
|
||||||
|
- gocyclo
|
||||||
|
- gomoddirectives
|
||||||
|
- gosec
|
||||||
|
- gosmopolitan
|
||||||
|
- ireturn
|
||||||
|
- lll
|
||||||
|
- maintidx
|
||||||
|
- makezero
|
||||||
|
- mnd
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nilnil
|
||||||
|
- nlreturn
|
||||||
|
- noctx
|
||||||
|
- nonamedreturns
|
||||||
|
- paralleltest
|
||||||
|
- prealloc
|
||||||
|
- 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 +101,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.416
|
||||||
|
- 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.29
|
||||||
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.1-alpine AS deps
|
FROM golang:1.24.3-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.
|
||||||
|
|
127
Makefile
127
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)
|
||||||
|
@ -27,24 +31,22 @@ endif
|
||||||
ifeq ($(debug), 1)
|
ifeq ($(debug), 1)
|
||||||
CGO_ENABLED = 0
|
CGO_ENABLED = 0
|
||||||
GODOXY_DEBUG = 1
|
GODOXY_DEBUG = 1
|
||||||
BUILD_FLAGS += -gcflags=all='-N -l'
|
BUILD_FLAGS += -gcflags=all='-N -l' -tags debug
|
||||||
endif
|
else ifeq ($(pprof), 1)
|
||||||
|
|
||||||
ifeq ($(pprof), 1)
|
|
||||||
CGO_ENABLED = 1
|
CGO_ENABLED = 1
|
||||||
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
||||||
BUILD_FLAGS = -tags pprof
|
BUILD_FLAGS += -tags pprof
|
||||||
VERSION := ${VERSION}-pprof
|
VERSION := ${VERSION}-pprof
|
||||||
else
|
else
|
||||||
CGO_ENABLED = 0
|
CGO_ENABLED = 0
|
||||||
LDFLAGS += -s -w
|
LDFLAGS += -s -w
|
||||||
BUILD_FLAGS = -pgo=auto -tags production
|
BUILD_FLAGS += -pgo=auto -tags production
|
||||||
endif
|
endif
|
||||||
|
|
||||||
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
|
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
|
||||||
|
BIN_PATH := $(shell pwd)/bin/${NAME}
|
||||||
|
|
||||||
export NAME
|
export NAME
|
||||||
export CMD_PATH
|
|
||||||
export CGO_ENABLED
|
export CGO_ENABLED
|
||||||
export GODOXY_DEBUG
|
export GODOXY_DEBUG
|
||||||
export GODOXY_TRACE
|
export GODOXY_TRACE
|
||||||
|
@ -52,25 +54,69 @@ export GODEBUG
|
||||||
export GORACE
|
export GORACE
|
||||||
export BUILD_FLAGS
|
export BUILD_FLAGS
|
||||||
|
|
||||||
|
ifeq ($(shell id -u), 0)
|
||||||
|
SETCAP_CMD = setcap
|
||||||
|
else
|
||||||
|
SETCAP_CMD = sudo setcap
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
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
|
||||||
if [ $(shell id -u) -eq 0 ]; \
|
${POST_BUILD}
|
||||||
then setcap CAP_NET_BIND_SERVICE=+eip bin/${NAME}; \
|
|
||||||
else sudo setcap CAP_NET_BIND_SERVICE=+eip bin/${NAME}; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
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:
|
||||||
|
make NAME="godoxy-test" debug=1 build
|
||||||
|
sh -c 'HTTP_ADDR=:81 HTTPS_ADDR=:8443 API_ADDR=:8899 DEBUG=1 bin/godoxy-test'
|
||||||
|
|
||||||
mtrace:
|
mtrace:
|
||||||
bin/godoxy debug-ls-mtrace > mtrace.json
|
${BIN_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 &&\
|
||||||
|
@ -85,48 +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
|
|
||||||
|
|
||||||
# To generate schema
|
|
||||||
# comment out this part from typescript-json-schema.js#L884
|
|
||||||
#
|
|
||||||
# if (indexType.flags !== ts.TypeFlags.Number && !isIndexedObject) {
|
|
||||||
# throw new Error("Not supported: IndexSignatureDeclaration with index symbol other than a number or a string");
|
|
||||||
# }
|
|
||||||
|
|
||||||
gen-schema-single:
|
|
||||||
bun --bun run typescript-json-schema --noExtraProps --required --skipLibCheck --tsNodeRegister=true -o schemas/${OUT} schemas/${IN} ${CLASS}
|
|
||||||
# minify
|
|
||||||
python3 -c "import json; f=open('schemas/${OUT}', 'r'); j=json.load(f); f.close(); f=open('schemas/${OUT}', 'w'); json.dump(j, f, separators=(',', ':'));"
|
|
||||||
|
|
||||||
gen-schema:
|
|
||||||
cd schemas && bun --bun tsc
|
|
||||||
make IN=config/config.ts \
|
|
||||||
CLASS=Config \
|
|
||||||
OUT=config.schema.json \
|
|
||||||
gen-schema-single
|
|
||||||
make IN=providers/routes.ts \
|
|
||||||
CLASS=Routes \
|
|
||||||
OUT=routes.schema.json \
|
|
||||||
gen-schema-single
|
|
||||||
make IN=middlewares/middleware_compose.ts \
|
|
||||||
CLASS=MiddlewareCompose \
|
|
||||||
OUT=middleware_compose.schema.json \
|
|
||||||
gen-schema-single
|
|
||||||
make IN=docker.ts \
|
|
||||||
CLASS=DockerRoutes \
|
|
||||||
OUT=docker_routes.schema.json \
|
|
||||||
gen-schema-single
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
publish-schema:
|
|
||||||
cd schemas && bun publish && cd ..
|
|
||||||
|
|
||||||
update-schema-generator:
|
|
||||||
pnpm up -g typescript-json-schema
|
|
||||||
|
|
||||||
push-github:
|
push-github:
|
||||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
110
README.md
110
README.md
|
@ -2,20 +2,19 @@
|
||||||
|
|
||||||
# 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>
|
||||||
|
|
||||||
<!-- [](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy) -->
|
|
||||||
|
|
||||||
<img src="screenshots/webui.jpg" style="max-width: 650">
|
<img src="screenshots/webui.jpg" style="max-width: 650">
|
||||||
|
|
||||||
|
@ -27,10 +26,11 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
|
||||||
|
|
||||||
- [GoDoxy](#godoxy)
|
- [GoDoxy](#godoxy)
|
||||||
- [Table of content](#table-of-content)
|
- [Table of content](#table-of-content)
|
||||||
|
- [Running demo](#running-demo)
|
||||||
- [Key Features](#key-features)
|
- [Key Features](#key-features)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [How does GoDoxy work](#how-does-godoxy-work)
|
|
||||||
- [Setup](#setup)
|
- [Setup](#setup)
|
||||||
|
- [How does GoDoxy work](#how-does-godoxy-work)
|
||||||
- [Screenshots](#screenshots)
|
- [Screenshots](#screenshots)
|
||||||
- [idlesleeper](#idlesleeper)
|
- [idlesleeper](#idlesleeper)
|
||||||
- [Metrics and Logs](#metrics-and-logs)
|
- [Metrics and Logs](#metrics-and-logs)
|
||||||
|
@ -38,46 +38,60 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
|
||||||
- [Folder structrue](#folder-structrue)
|
- [Folder structrue](#folder-structrue)
|
||||||
- [Build it yourself](#build-it-yourself)
|
- [Build it yourself](#build-it-yourself)
|
||||||
|
|
||||||
|
## Running demo
|
||||||
|
|
||||||
|
<https://demo.godoxy.dev>
|
||||||
|
|
||||||
|
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- Easy to use
|
- **Simple**
|
||||||
- Effortless configuration
|
- Effortless configuration with [simple labels](https://docs.godoxy.dev/Docker-labels-and-Route-Files) or WebUI
|
||||||
- Simple multi-node setup with GoDoxy agents or Docker Socket Proxies
|
- [Simple multi-node setup](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
|
||||||
- Error messages is clear and detailed, easy troubleshooting
|
- Detailed error messages for easy troubleshooting.
|
||||||
- Auto SSL with Let's Encrypt (See [Supported DNS-01 Challenge Providers](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
- **ACL**: connection / request level access control
|
||||||
- Auto hot-reload on container state / config file changes
|
- IP/CIDR
|
||||||
- Container aware: create routes dynamically from running docker containers
|
- Country **(Maxmind account required)**
|
||||||
- **idlesleeper**: stop and wake containers based on traffic _(optional, see [screenshots](#idlesleeper))_
|
- Timezone **(Maxmind account required)**
|
||||||
- HTTP reserve proxy and TCP/UDP port forwarding
|
- **Access logging**
|
||||||
- OpenID Connect integration: SSO and secure your apps easily
|
- **Advanced Automation**
|
||||||
- [HTTP middleware](https://github.com/yusing/go-proxy/wiki/Middlewares) and [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://docs.godoxy.dev/DNS-01-Providers))
|
||||||
- **Web UI with App dashboard, config editor, _uptime and system metrics_, _docker logs viewer_**
|
- Auto-configuration for Docker containers
|
||||||
- Supports linux/amd64 and linux/arm64
|
- Hot-reloading of configurations and container state changes
|
||||||
|
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
||||||
|
- Docker containers
|
||||||
|
- Proxmox LXCs
|
||||||
|
- **Traffic Management**
|
||||||
|
- HTTP reserve proxy
|
||||||
|
- TCP/UDP port forwarding
|
||||||
|
- **OpenID Connect support**: SSO and secure your apps easily
|
||||||
|
- **Customization**
|
||||||
|
- [HTTP middlewares](https://docs.godoxy.dev/Middlewares)
|
||||||
|
- [Custom error pages support](https://docs.godoxy.dev/Custom-Error-Pages)
|
||||||
|
- **Web UI**
|
||||||
|
- App Dashboard
|
||||||
|
- Config Editor
|
||||||
|
- Uptime and System Metrics
|
||||||
|
- Docker Logs Viewer
|
||||||
|
- **Cross-Platform support**
|
||||||
|
- Supports **linux/amd64** and **linux/arm64**
|
||||||
|
- **Efficient and Performant**
|
||||||
- Written in **[Go](https://go.dev)**
|
- Written in **[Go](https://go.dev)**
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Setup Wildcard DNS Record(s) for machine running `GoDoxy`, e.g.
|
Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
||||||
|
|
||||||
- A Record: `*.domain.com` -> `10.0.10.1`
|
- A Record: `*.domain.com` -> `10.0.10.1`
|
||||||
- AAAA Record (if you use IPv6): `*.domain.com` -> `::ffff:a00:a01`
|
- AAAA Record (if you use IPv6): `*.domain.com` -> `::ffff:a00:a01`
|
||||||
|
|
||||||
## How does GoDoxy work
|
|
||||||
|
|
||||||
1. List all the containers
|
|
||||||
2. Read container name, labels and port configurations for each of them
|
|
||||||
3. Create a route if applicable (a route is like a "Virtual Host" in NPM)
|
|
||||||
4. Watch for container / config changes and update automatically
|
|
||||||
|
|
||||||
GoDoxy uses the label `proxy.aliases` as the subdomain(s), if unset it defaults to the `container_name` field in docker compose.
|
|
||||||
|
|
||||||
For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
**NOTE:** GoDoxy is designed to be (and only works when) running in `host` network mode, do not change it. To change listening ports, modify `.env`.
|
> [!NOTE]
|
||||||
|
> GoDoxy is designed to be running in `host` network mode, do not change it.
|
||||||
|
>
|
||||||
|
> To change listening ports, modify `.env`.
|
||||||
|
|
||||||
1. Prepare a new directory for docker compose and config files.
|
1. Prepare a new directory for docker compose and config files.
|
||||||
|
|
||||||
|
@ -87,11 +101,25 @@ For example, with the label `proxy.aliases: qbt` you can access your app via `qb
|
||||||
/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. Start the container `docker compose up -d` and wait for it to be ready
|
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`
|
4. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
## How does GoDoxy work
|
||||||
|
|
||||||
|
1. List all the containers
|
||||||
|
2. Read container name, labels and port configurations for each of them
|
||||||
|
3. Create a route if applicable (a route is like a "Virtual Host" in NPM)
|
||||||
|
4. Watch for container / config changes and update automatically
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> GoDoxy uses the label `proxy.aliases` as the subdomain(s), if unset it defaults to the `container_name` field in docker compose.
|
||||||
|
>
|
||||||
|
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
@ -124,8 +152,6 @@ For example, with the label `proxy.aliases: qbt` you can access your app via `qb
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
|
||||||
|
|
||||||
## Manual Setup
|
## Manual Setup
|
||||||
|
|
||||||
1. Make `config` directory then grab `config.example.yml` into `config/config.yml`
|
1. Make `config` directory then grab `config.example.yml` into `config/config.yml`
|
||||||
|
|
|
@ -5,17 +5,16 @@
|
||||||
[](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>
|
||||||
|
|
||||||
<!-- [](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
<h5><a href="README.md">EN</a> | 中文</h5>
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy) -->
|
|
||||||
|
|
||||||
<a href="README.md">EN</a> | **中文**
|
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
||||||
|
|
||||||
|
@ -27,6 +26,7 @@
|
||||||
|
|
||||||
- [GoDoxy](#godoxy)
|
- [GoDoxy](#godoxy)
|
||||||
- [目錄](#目錄)
|
- [目錄](#目錄)
|
||||||
|
- [運行示例](#運行示例)
|
||||||
- [主要特點](#主要特點)
|
- [主要特點](#主要特點)
|
||||||
- [前置需求](#前置需求)
|
- [前置需求](#前置需求)
|
||||||
- [安裝](#安裝)
|
- [安裝](#安裝)
|
||||||
|
@ -37,22 +37,46 @@
|
||||||
- [監控](#監控)
|
- [監控](#監控)
|
||||||
- [自行編譯](#自行編譯)
|
- [自行編譯](#自行編譯)
|
||||||
|
|
||||||
|
## 運行示例
|
||||||
|
|
||||||
|
<https://demo.godoxy.dev>
|
||||||
|
|
||||||
|
[](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)** 語言編寫
|
||||||
|
|
||||||
[🔼 回到頂部](#目錄)
|
[🔼 回到頂部](#目錄)
|
||||||
|
|
||||||
|
@ -65,7 +89,10 @@
|
||||||
|
|
||||||
## 安裝
|
## 安裝
|
||||||
|
|
||||||
**注意:** GoDoxy 設計為(且僅在)`host` 網路模式下運作,請勿更改。如需更改監聽埠,請修改 `.env`。
|
> [!NOTE]
|
||||||
|
> GoDoxy 僅在 `host` 網路模式下運作,請勿更改。
|
||||||
|
>
|
||||||
|
> 如需更改監聽埠,請修改 `.env`。
|
||||||
|
|
||||||
1. 準備一個新目錄用於 docker compose 和配置文件。
|
1. 準備一個新目錄用於 docker compose 和配置文件。
|
||||||
|
|
||||||
|
@ -75,9 +102,7 @@
|
||||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 啟動容器 `docker compose up -d` 並等待就緒
|
3. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
|
||||||
|
|
||||||
4. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
|
|
||||||
|
|
||||||
[🔼 回到頂部](#目錄)
|
[🔼 回到頂部](#目錄)
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
|
|
||||||
"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"
|
||||||
"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())
|
|
||||||
|
|
||||||
ca := &agent.PEMPair{}
|
ca := &agent.PEMPair{}
|
||||||
err := ca.Load(env.AgentCACert)
|
err := ca.Load(env.AgentCACert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -55,6 +52,17 @@ Tips:
|
||||||
}
|
}
|
||||||
|
|
||||||
server.StartAgentServer(t, opts)
|
server.StartAgentServer(t, opts)
|
||||||
|
|
||||||
|
if socketproxy.ListenAddr != "" {
|
||||||
|
logging.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)
|
||||||
|
|
92
agent/go.mod
Normal file
92
agent/go.mod
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
module github.com/yusing/go-proxy/agent
|
||||||
|
|
||||||
|
go 1.24.3
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy => ..
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy/socketproxy => ../socket-proxy
|
||||||
|
|
||||||
|
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1
|
||||||
|
|
||||||
|
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250502022742-408a348f1b97
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/coder/websocket v1.8.13
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
github.com/yusing/go-proxy v0.0.0-00010101000000-000000000000
|
||||||
|
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/buger/goterm v1.0.4 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/diskfs/go-diskfs v1.6.0 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/djherbis/times v1.6.0 // indirect
|
||||||
|
github.com/docker/cli v28.1.1+incompatible // indirect
|
||||||
|
github.com/docker/docker v28.1.1+incompatible // indirect
|
||||||
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.8.3 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
|
github.com/go-acme/lego/v4 v4.23.1 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.17.1 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
|
||||||
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/gotify/server/v2 v2.6.3 // indirect
|
||||||
|
github.com/jinzhu/copier v0.4.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||||
|
github.com/luthermonson/go-proxmox v0.2.2 // indirect
|
||||||
|
github.com/magefile/mage v1.15.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/miekg/dns v1.1.66 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
|
github.com/puzpuzpuz/xsync/v4 v4.1.0 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.51.0 // indirect
|
||||||
|
github.com/samber/lo v1.50.0 // indirect
|
||||||
|
github.com/samber/slog-common v0.18.1 // indirect
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.4 // 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/automaxprocs v1.6.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.2 // indirect
|
||||||
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
|
golang.org/x/net v0.40.0 // indirect
|
||||||
|
golang.org/x/sync v0.14.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/text v0.25.0 // indirect
|
||||||
|
golang.org/x/time v0.11.0 // indirect
|
||||||
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
330
agent/go.sum
Normal file
330
agent/go.sum
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||||
|
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
|
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||||
|
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||||
|
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/diskfs/go-diskfs v1.6.0 h1:YmK5+vLSfkwC6kKKRTRPGaDGNF+Xh8FXeiNHwryDfu4=
|
||||||
|
github.com/diskfs/go-diskfs v1.6.0/go.mod h1:bRFumZeGFCO8C2KNswrQeuj2m1WCVr4Ms5IjWMczMDk=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
|
github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=
|
||||||
|
github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
|
||||||
|
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
|
||||||
|
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
|
github.com/go-acme/lego/v4 v4.23.1 h1:lZ5fGtGESA2L9FB8dNTvrQUq3/X4QOb8ExkKyY7LSV4=
|
||||||
|
github.com/go-acme/lego/v4 v4.23.1/go.mod h1:7UMVR7oQbIYw6V7mTgGwi4Er7B6Ww0c+c8feiBM0EgI=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
|
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||||
|
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
|
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
|
||||||
|
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1 h1:fsSqE28vU0PRkq9FdekirRoDBeYJ+UaJ9dTErdXflWg=
|
||||||
|
github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1/go.mod h1:av6ggKWQz6SEkFyShjDEgVqiIB0RHvEQNIkPeqgJEeE=
|
||||||
|
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250502022742-408a348f1b97 h1:i52gBYamrKs4DHT1+SiobW2im5UgTMVXK1KIL1djSeA=
|
||||||
|
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250502022742-408a348f1b97/go.mod h1:XvbfPmmrdpLrsKwj3irYkxt5ygyMcDsTQTJ7cnZ9RNQ=
|
||||||
|
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/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
|
||||||
|
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/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.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||||
|
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||||
|
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||||
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||||
|
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||||
|
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
|
github.com/luthermonson/go-proxmox v0.2.2 h1:BZ7VEj302wxw2i/EwTcyEiBzQib8teocB2SSkLHyySY=
|
||||||
|
github.com/luthermonson/go-proxmox v0.2.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||||
|
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||||
|
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
|
||||||
|
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
|
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||||
|
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
||||||
|
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
|
github.com/puzpuzpuz/xsync/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.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
|
||||||
|
github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
|
||||||
|
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
|
||||||
|
github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ=
|
||||||
|
github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk=
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.7.3 h1:/MkPDl/tJhijN2GvB1MWwBn2FU8RiL3rQ8gpXkQm2EY=
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.7.3/go.mod h1:oWU7WHof4Xp8VguiNO02r1a4VzkgoOyOZhY5CuRke60=
|
||||||
|
github.com/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/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
|
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||||
|
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4=
|
||||||
|
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||||
|
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
|
||||||
|
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||||
|
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.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/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
|
go.uber.org/mock v0.5.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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
|
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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
|
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
|
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||||
|
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
|
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||||
|
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||||
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
|
@ -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))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"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/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"
|
"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"
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AgentConfig struct {
|
type AgentConfig struct {
|
||||||
|
@ -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()
|
cfg.l = logging.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) {
|
||||||
|
logging.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.name, serverVersion, agentVersion)
|
||||||
|
}
|
||||||
|
|
||||||
logging.Info().Msgf("agent %q initialized", cfg.name)
|
logging.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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -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,14 +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/net/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
||||||
)
|
)
|
||||||
|
@ -19,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,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)
|
||||||
|
@ -41,32 +40,33 @@ 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.NewHTTPHealthChecker(types.NewURL(&url.URL{
|
result, err = monitor.NewHTTPHealthMonitor(&url.URL{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
Host: host,
|
Host: host,
|
||||||
Path: path,
|
Path: path,
|
||||||
}), defaultHealthConfig).CheckHealth()
|
}, defaultHealthConfig).CheckHealth()
|
||||||
case "tcp", "udp":
|
case "tcp", "udp":
|
||||||
host := query.Get("host")
|
host := query.Get("host")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
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
|
||||||
}
|
}
|
||||||
result, err = monitor.NewRawHealthChecker(types.NewURL(&url.URL{
|
if port != "" {
|
||||||
|
host = fmt.Sprintf("%s:%s", host, port)
|
||||||
|
}
|
||||||
|
result, err = monitor.NewRawHealthMonitor(&url.URL{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
Host: host,
|
Host: host,
|
||||||
}), defaultHealthConfig).CheckHealth()
|
}, defaultHealthConfig).CheckHealth()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -74,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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,49 +1,57 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
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 {
|
var dialer = &net.Dialer{KeepAlive: 1 * time.Second}
|
||||||
io.Writer
|
|
||||||
|
func dialDockerSocket(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
|
return dialer.DialContext(ctx, "unix", env.DockerSocket)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (NopWriteCloser) Close() error {
|
func dockerSocketHandler() http.HandlerFunc {
|
||||||
return nil
|
rp := httputil.NewSingleHostReverseProxy(&url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: "api.moby.localhost",
|
||||||
|
})
|
||||||
|
rp.Transport = &http.Transport{
|
||||||
|
DialContext: dialDockerSocket,
|
||||||
|
}
|
||||||
|
return rp.ServeHTTP
|
||||||
}
|
}
|
||||||
|
|
||||||
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("/", dockerSocketHandler())
|
||||||
mux.ServeMux.HandleFunc("/", DockerSocketHandler())
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,23 +3,30 @@ 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"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 := strutils.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
|
isHTTPS, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
|
||||||
skipTLSVerify := strutils.ParseBool(r.Header.Get(agentproxy.HeaderXProxySkipTLSVerify))
|
skipTLSVerify, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxySkipTLSVerify))
|
||||||
responseHeaderTimeout, err := strconv.Atoi(r.Header.Get(agentproxy.HeaderXProxyResponseHeaderTimeout))
|
responseHeaderTimeout, err := strconv.Atoi(r.Header.Get(agentproxy.HeaderXProxyResponseHeaderTimeout))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
responseHeaderTimeout = 0
|
responseHeaderTimeout = 0
|
||||||
|
@ -35,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 {
|
||||||
|
@ -50,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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,5 +40,5 @@ func StartAgentServer(parent task.Parent, opt Options) {
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Start(parent, agentServer, logger)
|
server.Start(parent, agentServer, nil, logger)
|
||||||
}
|
}
|
||||||
|
|
98
cmd/main.go
98
cmd/main.go
|
@ -1,17 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal"
|
"github.com/yusing/go-proxy/internal/auth"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/query"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/config"
|
"github.com/yusing/go-proxy/internal/config"
|
||||||
|
"github.com/yusing/go-proxy/internal/dnsproviders"
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/homepage"
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
@ -19,13 +15,10 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
||||||
"github.com/yusing/go-proxy/internal/metrics/uptime"
|
"github.com/yusing/go-proxy/internal/metrics/uptime"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
||||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
|
||||||
"github.com/yusing/go-proxy/internal/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 {
|
||||||
|
@ -40,98 +33,29 @@ 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())
|
logging.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
|
||||||
logging.Trace().Msg("trace enabled")
|
logging.Trace().Msg("trace enabled")
|
||||||
parallel(
|
parallel(
|
||||||
internal.InitIconListCache,
|
dnsproviders.InitProviders,
|
||||||
homepage.InitOverridesConfig,
|
homepage.InitIconListCache,
|
||||||
favicon.InitIconCache,
|
|
||||||
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")
|
logging.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(routequery.RoutesByAlias())
|
|
||||||
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{
|
||||||
|
@ -158,11 +82,3 @@ func prepareDirectory(dir string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//go:build production
|
//go:build !pprof
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
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".
|
||||||
|
|
|
@ -17,12 +17,46 @@
|
||||||
|
|
||||||
# 3. other providers, see https://github.com/yusing/godoxy/wiki/Supported-DNS%E2%80%9001-Providers#supported-dns-01-providers
|
# 3. other providers, see https://github.com/yusing/godoxy/wiki/Supported-DNS%E2%80%9001-Providers#supported-dns-01-providers
|
||||||
|
|
||||||
|
# acl:
|
||||||
|
# default: allow # or deny (default: allow)
|
||||||
|
# allow_local: true # or false (default: true)
|
||||||
|
# allow:
|
||||||
|
# - ip:1.2.3.4
|
||||||
|
# - cidr:1.2.3.4/32
|
||||||
|
# - country:US
|
||||||
|
# - timezone:Asia/Shanghai
|
||||||
|
# deny:
|
||||||
|
# - ip:1.2.3.4
|
||||||
|
# - cidr:1.2.3.4/32
|
||||||
|
# - country:US
|
||||||
|
# - timezone:Asia/Shanghai
|
||||||
|
# log: # warning: logging ACL can be slow based on the number of incoming connections and configured rules
|
||||||
|
# buffer_size: 65536 # (default: 64KB)
|
||||||
|
# path: /app/logs/acl.log # (default: none)
|
||||||
|
# stdout: false # (default: false)
|
||||||
|
# keep: last 10 # (default: none)
|
||||||
|
|
||||||
entrypoint:
|
entrypoint:
|
||||||
# Below define an example of middleware config
|
# Below define an example of middleware config
|
||||||
# 1. block non local IP connections
|
# 1. 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"
|
||||||
|
@ -73,6 +107,14 @@ providers:
|
||||||
# url: https://discord.com/api/webhooks/...
|
# url: https://discord.com/api/webhooks/...
|
||||||
# template: discord # this means use payload template from internal/notif/templates/discord.json
|
# template: discord # this means use payload template from internal/notif/templates/discord.json
|
||||||
|
|
||||||
|
# Proxmox providers (for idlesleep support for proxmox LXCs)
|
||||||
|
#
|
||||||
|
# proxmox:
|
||||||
|
# - url: https://pve.domain.com:8006/api2/json
|
||||||
|
# token_id: root@pam!abcdef
|
||||||
|
# secret: aaaa-bbbb-cccc-dddd
|
||||||
|
# no_tls_verify: true
|
||||||
|
|
||||||
# Check https://github.com/yusing/godoxy/wiki/Certificates-and-domain-matching#domain-matching
|
# Check https://github.com/yusing/godoxy/wiki/Certificates-and-domain-matching#domain-matching
|
||||||
# for explaination of `match_domains`
|
# for explaination of `match_domains`
|
||||||
#
|
#
|
||||||
|
|
235
go.mod
235
go.mod
|
@ -1,96 +1,251 @@
|
||||||
module github.com/yusing/go-proxy
|
module github.com/yusing/go-proxy
|
||||||
|
|
||||||
go 1.24.1
|
go 1.24.3
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy/agent => ./agent
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy/internal/dnsproviders => ./internal/dnsproviders
|
||||||
|
|
||||||
|
replace github.com/coreos/go-oidc/v3 => github.com/godoxy-app/go-oidc/v3 v3.14.2
|
||||||
|
|
||||||
|
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1
|
||||||
|
|
||||||
|
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250502022742-408a348f1b97
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.10.2 // parsing HTML for extract fav icon
|
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
|
||||||
github.com/coder/websocket v1.8.13 // websocket for API and agent
|
github.com/coder/websocket v1.8.13 // websocket for API and agent
|
||||||
github.com/coreos/go-oidc/v3 v3.13.0 // oidc authentication
|
github.com/coreos/go-oidc/v3 v3.14.1 // oidc authentication
|
||||||
github.com/docker/docker v28.0.4+incompatible // docker daemon
|
github.com/docker/docker v28.1.1+incompatible // docker daemon
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // file watcher
|
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||||
github.com/go-acme/lego/v4 v4.22.2 // acme client
|
github.com/go-acme/lego/v4 v4.23.1 // acme client
|
||||||
github.com/go-playground/validator/v10 v10.25.0 // validator
|
github.com/go-playground/validator/v10 v10.26.0 // validator
|
||||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 // jwt for default auth
|
github.com/gotify/server/v2 v2.6.3 // reference the Message struct for json response
|
||||||
github.com/gotify/server/v2 v2.6.1 // reference the Message struct for json response
|
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||||
github.com/prometheus/client_golang v1.21.1 // metrics
|
github.com/puzpuzpuz/xsync/v4 v4.1.0 // lock free map for concurrent operations
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // lock free map for concurrent operations
|
|
||||||
github.com/rs/zerolog v1.34.0 // logging
|
github.com/rs/zerolog v1.34.0 // logging
|
||||||
github.com/shirou/gopsutil/v4 v4.25.2 // system info metrics
|
github.com/shirou/gopsutil/v4 v4.25.4 // 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.36.0 // encrypting password with bcrypt
|
golang.org/x/crypto v0.38.0 // encrypting password with bcrypt
|
||||||
golang.org/x/net v0.38.0 // HTTP header utilities
|
golang.org/x/net v0.40.0 // HTTP header utilities
|
||||||
golang.org/x/oauth2 v0.28.0 // oauth2 authentication
|
golang.org/x/oauth2 v0.30.0 // oauth2 authentication
|
||||||
golang.org/x/text v0.23.0 // string utilities
|
|
||||||
golang.org/x/time v0.11.0 // time utilities
|
golang.org/x/time v0.11.0 // time utilities
|
||||||
gopkg.in/yaml.v3 v3.0.1 // yaml parsing for different config files
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/docker/cli v28.0.4+incompatible
|
github.com/docker/cli v28.1.1+incompatible
|
||||||
github.com/docker/go-connections v0.5.0
|
github.com/goccy/go-yaml v1.17.1 // yaml parsing for different config files
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
|
github.com/luthermonson/go-proxmox v0.2.2
|
||||||
|
github.com/oschwald/maxminddb-golang v1.13.1
|
||||||
|
github.com/quic-go/quic-go v0.51.0
|
||||||
|
github.com/samber/slog-zerolog/v2 v2.7.3
|
||||||
|
github.com/spf13/afero v1.14.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
|
github.com/yusing/go-proxy/agent v0.0.0-00010101000000-000000000000
|
||||||
|
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-00010101000000-000000000000
|
||||||
|
go.uber.org/atomic v1.11.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
cloud.google.com/go/auth v0.16.1 // indirect
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||||
|
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||||
|
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
||||||
|
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.2 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.51.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||||
|
github.com/aws/smithy-go v1.22.3 // indirect
|
||||||
|
github.com/baidubce/bce-sdk-go v0.9.226 // indirect
|
||||||
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
|
github.com/boombuler/barcode v1.0.2 // indirect
|
||||||
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/civo/civogo v0.5.0 // indirect
|
||||||
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
|
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/diskfs/go-diskfs v1.6.0 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/djherbis/times v1.6.0 // indirect
|
||||||
|
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||||
|
github.com/docker/go-connections v0.5.0
|
||||||
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.3 // indirect
|
||||||
|
github.com/exoscale/egoscale/v3 v3.1.17 // 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/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
|
github.com/go-errors/errors v1.5.1 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect; indirectindirect
|
||||||
|
github.com/gofrs/flock v0.12.1 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
|
||||||
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
|
||||||
|
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
||||||
|
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||||
|
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||||
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.149 // indirect
|
||||||
|
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||||
|
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
|
||||||
|
github.com/jinzhu/copier v0.4.0 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
||||||
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||||
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
|
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||||
|
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/linode/linodego v1.50.0 // indirect
|
||||||
|
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
|
||||||
|
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||||
|
github.com/magefile/mage v1.15.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/miekg/dns v1.1.64 // indirect
|
github.com/miekg/dns v1.1.66 // indirect
|
||||||
|
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/term v0.5.0 // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||||
|
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||||
|
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
|
||||||
|
github.com/nrdcg/desec v0.11.0 // indirect
|
||||||
|
github.com/nrdcg/freemyip v0.3.0 // indirect
|
||||||
|
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||||
|
github.com/nrdcg/goinwx v0.11.0 // indirect
|
||||||
|
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||||
|
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||||
|
github.com/nrdcg/nodion v0.1.0 // indirect
|
||||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||||
|
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
|
github.com/oracle/oci-go-sdk/v65 v65.91.0 // indirect
|
||||||
github.com/ovh/go-ovh v1.7.0 // indirect
|
github.com/ovh/go-ovh v1.7.0 // indirect
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/peterhellberg/link v1.2.0 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/pquerna/otp v1.4.0 // indirect
|
||||||
github.com/prometheus/common v0.63.0 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/prometheus/procfs v0.16.0 // indirect
|
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
github.com/sacloud/api-client-go v0.2.10 // indirect
|
||||||
|
github.com/sacloud/go-http v0.1.9 // indirect
|
||||||
|
github.com/sacloud/iaas-api-go v1.15.0 // indirect
|
||||||
|
github.com/sacloud/packages-go v0.0.11 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
|
github.com/samber/lo v1.50.0 // indirect
|
||||||
|
github.com/samber/slog-common v0.18.1 // indirect
|
||||||
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 // indirect
|
||||||
|
github.com/selectel/domains-go v1.1.0 // indirect
|
||||||
|
github.com/selectel/go-selvpcclient/v3 v3.2.1 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||||
|
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||||
|
github.com/softlayer/softlayer-go v1.1.7 // indirect
|
||||||
|
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||||
|
github.com/sony/gobreaker v1.0.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/cast v1.8.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
|
github.com/spf13/viper v1.20.1 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1164 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 // indirect
|
||||||
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
|
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||||
|
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
||||||
|
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||||
|
github.com/volcengine/volc-sdk-golang v1.0.207 // indirect
|
||||||
|
github.com/vultr/govultr/v3 v3.20.0 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
|
||||||
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.2 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/ratelimit v0.3.1 // indirect
|
||||||
golang.org/x/mod v0.24.0 // indirect
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
golang.org/x/sync v0.12.0 // indirect
|
golang.org/x/sync v0.14.0 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/tools v0.31.0 // indirect
|
golang.org/x/text v0.25.0
|
||||||
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
|
google.golang.org/api v0.233.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
|
||||||
|
google.golang.org/grpc v1.72.1 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gotest.tools/v3 v3.5.1 // indirect
|
gopkg.in/ns1/ns1-go.v2 v2.14.3 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
k8s.io/api v0.33.0 // indirect
|
||||||
|
k8s.io/apimachinery v0.33.0 // indirect
|
||||||
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
|
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect
|
||||||
|
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||||
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
166
internal/acl/config.go
Normal file
166
internal/acl/config.go
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging/accesslog"
|
||||||
|
"github.com/yusing/go-proxy/internal/maxmind"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Default string `json:"default" validate:"omitempty,oneof=allow deny"` // default: allow
|
||||||
|
AllowLocal *bool `json:"allow_local"` // default: true
|
||||||
|
Allow Matchers `json:"allow"`
|
||||||
|
Deny Matchers `json:"deny"`
|
||||||
|
Log *accesslog.ACLLoggerConfig `json:"log"`
|
||||||
|
|
||||||
|
config
|
||||||
|
valErr gperr.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
defaultAllow bool
|
||||||
|
allowLocal bool
|
||||||
|
ipCache *xsync.Map[string, *checkCache]
|
||||||
|
logAllowed bool
|
||||||
|
logger *accesslog.AccessLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
type checkCache struct {
|
||||||
|
*maxmind.IPInfo
|
||||||
|
allow bool
|
||||||
|
created time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheTTL = 1 * time.Minute
|
||||||
|
|
||||||
|
func (c *checkCache) Expired() bool {
|
||||||
|
return c.created.Add(cacheTTL).Before(utils.TimeNow())
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: add stats
|
||||||
|
|
||||||
|
const (
|
||||||
|
ACLAllow = "allow"
|
||||||
|
ACLDeny = "deny"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Config) Validate() gperr.Error {
|
||||||
|
switch c.Default {
|
||||||
|
case "", ACLAllow:
|
||||||
|
c.defaultAllow = true
|
||||||
|
case ACLDeny:
|
||||||
|
c.defaultAllow = false
|
||||||
|
default:
|
||||||
|
c.valErr = gperr.New("invalid default value").Subject(c.Default)
|
||||||
|
return c.valErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.AllowLocal != nil {
|
||||||
|
c.allowLocal = *c.AllowLocal
|
||||||
|
} else {
|
||||||
|
c.allowLocal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Log != nil {
|
||||||
|
c.logAllowed = c.Log.LogAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.allowLocal && !c.defaultAllow && len(c.Allow) == 0 {
|
||||||
|
c.valErr = gperr.New("allow_local is false and default is deny, but no allow rules are configured")
|
||||||
|
return c.valErr
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ipCache = xsync.NewMap[string, *checkCache]()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Valid() bool {
|
||||||
|
return c != nil && c.valErr == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Start(parent *task.Task) gperr.Error {
|
||||||
|
if c.Log != nil {
|
||||||
|
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
||||||
|
if err != nil {
|
||||||
|
return gperr.New("failed to start access logger").With(err)
|
||||||
|
}
|
||||||
|
c.logger = logger
|
||||||
|
}
|
||||||
|
if c.valErr != nil {
|
||||||
|
return c.valErr
|
||||||
|
}
|
||||||
|
logging.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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
|
||||||
|
if common.ForceResolveCountry && info.City == nil {
|
||||||
|
maxmind.LookupCity(info)
|
||||||
|
}
|
||||||
|
c.ipCache.Store(info.Str, &checkCache{
|
||||||
|
IPInfo: info,
|
||||||
|
allow: allow,
|
||||||
|
created: utils.TimeNow(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *config) log(info *maxmind.IPInfo, allowed bool) {
|
||||||
|
if c.logger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !allowed || c.logAllowed {
|
||||||
|
c.logger.LogACL(info, !allowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) IPAllowed(ip net.IP) bool {
|
||||||
|
if ip == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// always allow loopback, not logged
|
||||||
|
if ip.IsLoopback() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.allowLocal && ip.IsPrivate() {
|
||||||
|
c.log(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ipStr := ip.String()
|
||||||
|
record, ok := c.ipCache.Load(ipStr)
|
||||||
|
if ok && !record.Expired() {
|
||||||
|
c.log(record.IPInfo, record.allow)
|
||||||
|
return record.allow
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
|
||||||
|
if c.Allow.Match(ipAndStr) {
|
||||||
|
c.log(ipAndStr, true)
|
||||||
|
c.cacheRecord(ipAndStr, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if c.Deny.Match(ipAndStr) {
|
||||||
|
c.log(ipAndStr, false)
|
||||||
|
c.cacheRecord(ipAndStr, false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
c.log(ipAndStr, c.defaultAllow)
|
||||||
|
c.cacheRecord(ipAndStr, c.defaultAllow)
|
||||||
|
return c.defaultAllow
|
||||||
|
}
|
112
internal/acl/matcher.go
Normal file
112
internal/acl/matcher.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/maxmind"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MatcherFunc func(*maxmind.IPInfo) bool
|
||||||
|
|
||||||
|
type Matcher struct {
|
||||||
|
match MatcherFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type Matchers []Matcher
|
||||||
|
|
||||||
|
const (
|
||||||
|
MatcherTypeIP = "ip"
|
||||||
|
MatcherTypeCIDR = "cidr"
|
||||||
|
MatcherTypeTimeZone = "tz"
|
||||||
|
MatcherTypeCountry = "country"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: use this error in the future
|
||||||
|
//
|
||||||
|
//nolint:unused
|
||||||
|
var errMatcherFormat = gperr.Multiline().AddLines(
|
||||||
|
"invalid matcher format, expect {type}:{value}",
|
||||||
|
"Available types: ip|cidr|tz|country",
|
||||||
|
"ip:127.0.0.1",
|
||||||
|
"cidr:127.0.0.0/8",
|
||||||
|
"tz:Asia/Shanghai",
|
||||||
|
"country:GB",
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errSyntax = gperr.New("syntax error")
|
||||||
|
errInvalidIP = gperr.New("invalid IP")
|
||||||
|
errInvalidCIDR = gperr.New("invalid CIDR")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (matcher *Matcher) Parse(s string) error {
|
||||||
|
parts := strings.Split(s, ":")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return errSyntax
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parts[0] {
|
||||||
|
case MatcherTypeIP:
|
||||||
|
ip := net.ParseIP(parts[1])
|
||||||
|
if ip == nil {
|
||||||
|
return errInvalidIP
|
||||||
|
}
|
||||||
|
matcher.match = matchIP(ip)
|
||||||
|
case MatcherTypeCIDR:
|
||||||
|
_, net, err := net.ParseCIDR(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return errInvalidCIDR
|
||||||
|
}
|
||||||
|
matcher.match = matchCIDR(net)
|
||||||
|
case MatcherTypeTimeZone:
|
||||||
|
matcher.match = matchTimeZone(parts[1])
|
||||||
|
case MatcherTypeCountry:
|
||||||
|
matcher.match = matchISOCode(parts[1])
|
||||||
|
default:
|
||||||
|
return errSyntax
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (matchers Matchers) Match(ip *maxmind.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchCIDR(n *net.IPNet) MatcherFunc {
|
||||||
|
return func(ip *maxmind.IPInfo) bool {
|
||||||
|
return n.Contains(ip.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchTimeZone(tz string) MatcherFunc {
|
||||||
|
return func(ip *maxmind.IPInfo) bool {
|
||||||
|
city, ok := maxmind.LookupCity(ip)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return city.Location.TimeZone == tz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchISOCode(iso string) MatcherFunc {
|
||||||
|
return func(ip *maxmind.IPInfo) bool {
|
||||||
|
city, ok := maxmind.LookupCity(ip)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return city.Country.IsoCode == iso
|
||||||
|
}
|
||||||
|
}
|
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/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMatchers(t *testing.T) {
|
||||||
|
strMatchers := []string{
|
||||||
|
"ip:127.0.0.1",
|
||||||
|
"cidr:10.0.0.0/8",
|
||||||
|
}
|
||||||
|
|
||||||
|
var mathers Matchers
|
||||||
|
err := utils.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
internal/acl/tcp_listener.go
Normal file
59
internal/acl/tcp_listener.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TCPListener struct {
|
||||||
|
acl *Config
|
||||||
|
lis net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
type noConn struct{}
|
||||||
|
|
||||||
|
func (noConn) Read(b []byte) (int, error) { return 0, io.EOF }
|
||||||
|
func (noConn) Write(b []byte) (int, error) { return 0, io.EOF }
|
||||||
|
func (noConn) Close() error { return nil }
|
||||||
|
func (noConn) LocalAddr() net.Addr { return nil }
|
||||||
|
func (noConn) RemoteAddr() net.Addr { return nil }
|
||||||
|
func (noConn) SetDeadline(t time.Time) error { return nil }
|
||||||
|
func (noConn) SetReadDeadline(t time.Time) error { return nil }
|
||||||
|
func (noConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||||
|
|
||||||
|
func (cfg *Config) WrapTCP(lis net.Listener) net.Listener {
|
||||||
|
if cfg == nil {
|
||||||
|
return lis
|
||||||
|
}
|
||||||
|
return &TCPListener{
|
||||||
|
acl: cfg,
|
||||||
|
lis: lis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPListener) Addr() net.Addr {
|
||||||
|
return s.lis.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPListener) Accept() (net.Conn, error) {
|
||||||
|
c, err := s.lis.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
addr, ok := c.RemoteAddr().(*net.TCPAddr)
|
||||||
|
if !ok {
|
||||||
|
// Not a TCPAddr, drop
|
||||||
|
c.Close()
|
||||||
|
return noConn{}, nil
|
||||||
|
}
|
||||||
|
if !s.acl.IPAllowed(addr.IP) {
|
||||||
|
c.Close()
|
||||||
|
return noConn{}, nil
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TCPListener) Close() error {
|
||||||
|
return s.lis.Close()
|
||||||
|
}
|
79
internal/acl/udp_listener.go
Normal file
79
internal/acl/udp_listener.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UDPListener struct {
|
||||||
|
acl *Config
|
||||||
|
lis net.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WrapUDP(lis net.PacketConn) net.PacketConn {
|
||||||
|
if c == nil {
|
||||||
|
return lis
|
||||||
|
}
|
||||||
|
return &UDPListener{
|
||||||
|
acl: c,
|
||||||
|
lis: lis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) LocalAddr() net.Addr {
|
||||||
|
return s.lis.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||||
|
for {
|
||||||
|
n, addr, err := s.lis.ReadFrom(p)
|
||||||
|
if err != nil {
|
||||||
|
return n, addr, err
|
||||||
|
}
|
||||||
|
udpAddr, ok := addr.(*net.UDPAddr)
|
||||||
|
if !ok {
|
||||||
|
// Not a UDPAddr, drop
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !s.acl.IPAllowed(udpAddr.IP) {
|
||||||
|
// Drop packet from disallowed IP
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return n, addr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||||
|
for {
|
||||||
|
n, err := s.lis.WriteTo(p, addr)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
udpAddr, ok := addr.(*net.UDPAddr)
|
||||||
|
if !ok {
|
||||||
|
// Not a UDPAddr, drop
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !s.acl.IPAllowed(udpAddr.IP) {
|
||||||
|
// Drop packet to disallowed IP
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) SetDeadline(t time.Time) error {
|
||||||
|
return s.lis.SetDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) SetReadDeadline(t time.Time) error {
|
||||||
|
return s.lis.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) SetWriteDeadline(t time.Time) error {
|
||||||
|
return s.lis.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UDPListener) Close() error {
|
||||||
|
return s.lis.Close()
|
||||||
|
}
|
|
@ -4,19 +4,18 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/certapi"
|
"github.com/yusing/go-proxy/internal/api/v1/certapi"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/dockerapi"
|
"github.com/yusing/go-proxy/internal/api/v1/dockerapi"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/auth"
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
|
||||||
"github.com/yusing/go-proxy/internal/logging/memlogger"
|
"github.com/yusing/go-proxy/internal/logging/memlogger"
|
||||||
"github.com/yusing/go-proxy/internal/metrics/uptime"
|
"github.com/yusing/go-proxy/internal/metrics/uptime"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp/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 (
|
||||||
|
@ -47,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)
|
||||||
}
|
}
|
||||||
|
@ -68,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)
|
||||||
|
@ -93,26 +98,14 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||||
mux.HandleFunc("GET", "/v1/docker/logs/{server}/{container}", dockerapi.Logs, true)
|
mux.HandleFunc("GET", "/v1/docker/logs/{server}/{container}", dockerapi.Logs, true)
|
||||||
mux.HandleFunc("GET", "/v1/docker/containers", dockerapi.Containers, true)
|
mux.HandleFunc("GET", "/v1/docker/containers", dockerapi.Containers, true)
|
||||||
|
|
||||||
if common.PrometheusEnabled {
|
|
||||||
mux.Handle("GET /v1/metrics", promhttp.Handler())
|
|
||||||
logging.Info().Msg("prometheus metrics enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultAuth := auth.GetDefaultAuth()
|
defaultAuth := auth.GetDefaultAuth()
|
||||||
if defaultAuth != nil {
|
if defaultAuth == nil {
|
||||||
mux.HandleFunc("GET", "/v1/auth/redirect", defaultAuth.RedirectLoginPage)
|
return mux
|
||||||
mux.HandleFunc("GET", "/v1/auth/check", func(w http.ResponseWriter, r *http.Request) {
|
}
|
||||||
if err := defaultAuth.CheckToken(r); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
mux.HandleFunc("GET", "/v1/auth/check", auth.AuthCheckHandler)
|
||||||
return
|
mux.HandleFunc("GET,POST", "/v1/auth/redirect", defaultAuth.LoginHandler)
|
||||||
}
|
mux.HandleFunc("GET,POST", "/v1/auth/callback", defaultAuth.PostAuthCallbackHandler)
|
||||||
})
|
mux.HandleFunc("GET,POST", "/v1/auth/logout", defaultAuth.LogoutHandler)
|
||||||
mux.HandleFunc("GET,POST", "/v1/auth/callback", defaultAuth.LoginCallbackHandler)
|
|
||||||
mux.HandleFunc("GET,POST", "/v1/auth/logout", defaultAuth.LogoutCallbackHandler)
|
|
||||||
} else {
|
|
||||||
mux.HandleFunc("GET", "/v1/auth/check", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,308 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
OIDCProvider struct {
|
|
||||||
oauthConfig *oauth2.Config
|
|
||||||
oidcProvider *oidc.Provider
|
|
||||||
oidcVerifier *oidc.IDTokenVerifier
|
|
||||||
oidcEndSessionURL *url.URL
|
|
||||||
allowedUsers []string
|
|
||||||
allowedGroups []string
|
|
||||||
isMiddleware bool
|
|
||||||
}
|
|
||||||
|
|
||||||
providerJSON struct {
|
|
||||||
oidc.ProviderConfig
|
|
||||||
EndSessionURL string `json:"end_session_endpoint"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const CookieOauthState = "godoxy_oidc_state"
|
|
||||||
|
|
||||||
const (
|
|
||||||
OIDCMiddlewareCallbackPath = "/auth/callback"
|
|
||||||
OIDCLogoutPath = "/auth/logout"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewOIDCProvider(issuerURL, clientID, clientSecret, redirectURL string, allowedUsers, allowedGroups []string) (*OIDCProvider, error) {
|
|
||||||
if len(allowedUsers)+len(allowedGroups) == 0 {
|
|
||||||
return nil, errors.New("OIDC users, groups, or both must not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
wellKnown := strings.TrimSuffix(issuerURL, "/") + "/.well-known/openid-configuration"
|
|
||||||
resp, err := gphttp.Get(wellKnown)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("oidc: unable to read response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("oidc: %s: %s", resp.Status, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
var p providerJSON
|
|
||||||
err = json.Unmarshal(body, &p)
|
|
||||||
if err != nil {
|
|
||||||
mimeType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
|
||||||
if err == nil && mimeType != "application/json" {
|
|
||||||
return nil, fmt.Errorf("oidc: unexpected content type: %q from OIDC provider discovery, have you configured the correct issuer URL?", mimeType)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.IssuerURL != issuerURL {
|
|
||||||
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuerURL, p.IssuerURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
var endSessionURL *url.URL
|
|
||||||
if p.EndSessionURL != "" {
|
|
||||||
endSessionURL, err = url.Parse(p.EndSessionURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("oidc: failed to parse end session URL: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
provider := p.NewProvider(context.Background())
|
|
||||||
return &OIDCProvider{
|
|
||||||
oauthConfig: &oauth2.Config{
|
|
||||||
ClientID: clientID,
|
|
||||||
ClientSecret: clientSecret,
|
|
||||||
RedirectURL: redirectURL,
|
|
||||||
Endpoint: provider.Endpoint(),
|
|
||||||
Scopes: strutils.CommaSeperatedList(common.OIDCScopes),
|
|
||||||
},
|
|
||||||
oidcProvider: provider,
|
|
||||||
oidcVerifier: provider.Verifier(&oidc.Config{
|
|
||||||
ClientID: clientID,
|
|
||||||
}),
|
|
||||||
oidcEndSessionURL: endSessionURL,
|
|
||||||
allowedUsers: allowedUsers,
|
|
||||||
allowedGroups: allowedGroups,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOIDCProviderFromEnv creates a new OIDCProvider from environment variables.
|
|
||||||
func NewOIDCProviderFromEnv() (*OIDCProvider, error) {
|
|
||||||
return NewOIDCProvider(
|
|
||||||
common.OIDCIssuerURL,
|
|
||||||
common.OIDCClientID,
|
|
||||||
common.OIDCClientSecret,
|
|
||||||
common.OIDCRedirectURL,
|
|
||||||
common.OIDCAllowedUsers,
|
|
||||||
common.OIDCAllowedGroups,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) TokenCookieName() string {
|
|
||||||
return "godoxy_oidc_token"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) SetIsMiddleware(enabled bool) {
|
|
||||||
auth.isMiddleware = enabled
|
|
||||||
auth.oauthConfig.RedirectURL = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) SetAllowedUsers(users []string) {
|
|
||||||
auth.allowedUsers = users
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) SetAllowedGroups(groups []string) {
|
|
||||||
auth.allowedGroups = groups
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) CheckToken(r *http.Request) error {
|
|
||||||
token, err := r.Cookie(auth.TokenCookieName())
|
|
||||||
if err != nil {
|
|
||||||
return ErrMissingToken
|
|
||||||
}
|
|
||||||
|
|
||||||
// checks for Expiry, Audience == ClientID, Issuer, etc.
|
|
||||||
idToken, err := auth.oidcVerifier.Verify(r.Context(), token.Value)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to verify ID token: %w: %w", ErrInvalidToken, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(idToken.Audience) == 0 {
|
|
||||||
return ErrInvalidToken
|
|
||||||
}
|
|
||||||
|
|
||||||
var claims struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
Username string `json:"preferred_username"`
|
|
||||||
Groups []string `json:"groups"`
|
|
||||||
}
|
|
||||||
if err := idToken.Claims(&claims); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse claims: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logical AND between allowed users and groups.
|
|
||||||
allowedUser := slices.Contains(auth.allowedUsers, claims.Username)
|
|
||||||
allowedGroup := len(utils.Intersect(claims.Groups, auth.allowedGroups)) > 0
|
|
||||||
if !allowedUser && !allowedGroup {
|
|
||||||
return ErrUserNotAllowed
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateState generates a random string for OIDC state.
|
|
||||||
const oidcStateLength = 32
|
|
||||||
|
|
||||||
func generateState() (string, error) {
|
|
||||||
b := make([]byte, oidcStateLength)
|
|
||||||
_, err := rand.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return base64.URLEncoding.EncodeToString(b)[:oidcStateLength], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedirectOIDC initiates the OIDC login flow.
|
|
||||||
func (auth *OIDCProvider) RedirectLoginPage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
state, err := generateState()
|
|
||||||
if err != nil {
|
|
||||||
gphttp.ServerError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: CookieOauthState,
|
|
||||||
Value: state,
|
|
||||||
MaxAge: 300,
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
Secure: common.APIJWTSecure,
|
|
||||||
Path: "/",
|
|
||||||
})
|
|
||||||
|
|
||||||
redirURL := auth.oauthConfig.AuthCodeURL(state)
|
|
||||||
if auth.isMiddleware {
|
|
||||||
u, err := r.URL.Parse(redirURL)
|
|
||||||
if err != nil {
|
|
||||||
gphttp.ServerError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
q := u.Query()
|
|
||||||
q.Set("redirect_uri", "https://"+r.Host+OIDCMiddlewareCallbackPath+q.Get("redirect_uri"))
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
redirURL = u.String()
|
|
||||||
}
|
|
||||||
http.Redirect(w, r, redirURL, http.StatusTemporaryRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) exchange(r *http.Request) (*oauth2.Token, error) {
|
|
||||||
if auth.isMiddleware {
|
|
||||||
cfg := *auth.oauthConfig
|
|
||||||
cfg.RedirectURL = "https://" + r.Host + OIDCMiddlewareCallbackPath
|
|
||||||
return cfg.Exchange(r.Context(), r.URL.Query().Get("code"))
|
|
||||||
}
|
|
||||||
return auth.oauthConfig.Exchange(r.Context(), r.URL.Query().Get("code"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// OIDCCallbackHandler handles the OIDC callback.
|
|
||||||
func (auth *OIDCProvider) LoginCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// For testing purposes, skip provider verification
|
|
||||||
if common.IsTest {
|
|
||||||
auth.handleTestCallback(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state, err := r.Cookie(CookieOauthState)
|
|
||||||
if err != nil {
|
|
||||||
gphttp.BadRequest(w, "missing state cookie")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := r.URL.Query()
|
|
||||||
if query.Get("state") != state.Value {
|
|
||||||
gphttp.BadRequest(w, "invalid oauth state")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
oauth2Token, err := auth.exchange(r)
|
|
||||||
if err != nil {
|
|
||||||
gphttp.ServerError(w, r, fmt.Errorf("failed to exchange token: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
|
||||||
if !ok {
|
|
||||||
gphttp.BadRequest(w, "missing id_token")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
idToken, err := auth.oidcVerifier.Verify(r.Context(), rawIDToken)
|
|
||||||
if err != nil {
|
|
||||||
gphttp.ServerError(w, r, fmt.Errorf("failed to verify ID token: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setTokenCookie(w, r, auth.TokenCookieName(), rawIDToken, time.Until(idToken.Expiry))
|
|
||||||
|
|
||||||
// Redirect to home page
|
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OIDCProvider) LogoutCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if auth.oidcEndSessionURL == nil {
|
|
||||||
DefaultLogoutCallbackHandler(auth, w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := r.Cookie(auth.TokenCookieName())
|
|
||||||
if err != nil {
|
|
||||||
gphttp.BadRequest(w, "missing token cookie")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
clearTokenCookie(w, r, auth.TokenCookieName())
|
|
||||||
|
|
||||||
logoutURL := *auth.oidcEndSessionURL
|
|
||||||
logoutURL.Query().Add("id_token_hint", token.Value)
|
|
||||||
|
|
||||||
http.Redirect(w, r, logoutURL.String(), http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleTestCallback handles OIDC callback in test environment.
|
|
||||||
func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Request) {
|
|
||||||
state, err := r.Cookie(CookieOauthState)
|
|
||||||
if err != nil {
|
|
||||||
gphttp.BadRequest(w, "missing state cookie")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.URL.Query().Get("state") != state.Value {
|
|
||||||
gphttp.BadRequest(w, "invalid oauth state")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create test JWT token
|
|
||||||
setTokenCookie(w, r, auth.TokenCookieName(), "test", time.Hour)
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Provider interface {
|
|
||||||
TokenCookieName() string
|
|
||||||
CheckToken(r *http.Request) error
|
|
||||||
RedirectLoginPage(w http.ResponseWriter, r *http.Request)
|
|
||||||
LoginCallbackHandler(w http.ResponseWriter, r *http.Request)
|
|
||||||
LogoutCallbackHandler(w http.ResponseWriter, r *http.Request)
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrMissingToken = gperr.New("missing token")
|
|
||||||
ErrInvalidToken = gperr.New("invalid token")
|
|
||||||
ErrUserNotAllowed = gperr.New("user not allowed")
|
|
||||||
)
|
|
||||||
|
|
||||||
// cookieFQDN returns the fully qualified domain name of the request host
|
|
||||||
// with subdomain stripped.
|
|
||||||
//
|
|
||||||
// If the request host does not have a subdomain,
|
|
||||||
// an empty string is returned
|
|
||||||
//
|
|
||||||
// "abc.example.com" -> "example.com"
|
|
||||||
// "example.com" -> ""
|
|
||||||
func cookieFQDN(r *http.Request) string {
|
|
||||||
host, _, err := net.SplitHostPort(r.Host)
|
|
||||||
if err != nil {
|
|
||||||
host = r.Host
|
|
||||||
}
|
|
||||||
parts := strutils.SplitRune(host, '.')
|
|
||||||
if len(parts) < 2 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
parts[0] = ""
|
|
||||||
return strutils.JoinRune(parts, '.')
|
|
||||||
}
|
|
||||||
|
|
||||||
func setTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: name,
|
|
||||||
Value: value,
|
|
||||||
MaxAge: int(ttl.Seconds()),
|
|
||||||
Domain: cookieFQDN(r),
|
|
||||||
HttpOnly: true,
|
|
||||||
Secure: common.APIJWTSecure,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
Path: "/",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func clearTokenCookie(w http.ResponseWriter, r *http.Request, name string) {
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: name,
|
|
||||||
Value: "",
|
|
||||||
MaxAge: -1,
|
|
||||||
Domain: cookieFQDN(r),
|
|
||||||
HttpOnly: true,
|
|
||||||
Secure: common.APIJWTSecure,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
Path: "/",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultLogoutCallbackHandler clears the token cookie and redirects to the login page..
|
|
||||||
func DefaultLogoutCallbackHandler(auth Provider, w http.ResponseWriter, r *http.Request) {
|
|
||||||
clearTokenCookie(w, r, auth.TokenCookieName())
|
|
||||||
auth.RedirectLoginPage(w, r)
|
|
||||||
}
|
|
|
@ -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) {
|
||||||
|
|
|
@ -25,7 +25,7 @@ func (d *dockerInfo) MarshalJSON() ([]byte, error) {
|
||||||
},
|
},
|
||||||
"images": d.Images,
|
"images": d.Images,
|
||||||
"n_cpu": d.NCPU,
|
"n_cpu": d.NCPU,
|
||||||
"memory": strutils.FormatByteSizeWithUnit(d.MemTotal),
|
"memory": strutils.FormatByteSize(d.MemTotal),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package dockerapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
@ -9,20 +10,19 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Logs(w http.ResponseWriter, r *http.Request) {
|
func Logs(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
server := r.PathValue("server")
|
server := r.PathValue("server")
|
||||||
containerID := r.PathValue("container")
|
containerID := r.PathValue("container")
|
||||||
stdout := strutils.ParseBool(query.Get("stdout"))
|
stdout, _ := strconv.ParseBool(query.Get("stdout"))
|
||||||
stderr := strutils.ParseBool(query.Get("stderr"))
|
stderr, _ := strconv.ParseBool(query.Get("stderr"))
|
||||||
since := query.Get("from")
|
since := query.Get("from")
|
||||||
until := query.Get("to")
|
until := query.Get("to")
|
||||||
levels := query.Get("levels") // TODO: implement levels
|
levels := query.Get("levels") // TODO: implement levels
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -98,7 +98,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)) {
|
||||||
|
@ -119,6 +119,6 @@ func serveHTTP[V any, T ResultType[V]](w http.ResponseWriter, r *http.Request, g
|
||||||
})
|
})
|
||||||
} 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,138 +0,0 @@
|
||||||
package favicon
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
|
||||||
route "github.com/yusing/go-proxy/internal/route/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type cacheEntry struct {
|
|
||||||
Icon []byte `json:"icon"`
|
|
||||||
LastAccess time.Time `json:"lastAccess"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// cache key can be absolute url or route name.
|
|
||||||
var (
|
|
||||||
iconCache = make(map[string]*cacheEntry)
|
|
||||||
iconCacheMu sync.RWMutex
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
iconCacheTTL = 3 * 24 * time.Hour
|
|
||||||
cleanUpInterval = time.Hour
|
|
||||||
)
|
|
||||||
|
|
||||||
func InitIconCache() {
|
|
||||||
iconCacheMu.Lock()
|
|
||||||
defer iconCacheMu.Unlock()
|
|
||||||
|
|
||||||
err := utils.LoadJSONIfExist(common.IconCachePath, &iconCache)
|
|
||||||
if err != nil {
|
|
||||||
logging.Error().Err(err).Msg("failed to load icon cache")
|
|
||||||
} else if len(iconCache) > 0 {
|
|
||||||
logging.Info().Int("count", len(iconCache)).Msg("icon cache loaded")
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
cleanupTicker := time.NewTicker(cleanUpInterval)
|
|
||||||
defer cleanupTicker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-task.RootContextCanceled():
|
|
||||||
return
|
|
||||||
case <-cleanupTicker.C:
|
|
||||||
pruneExpiredIconCache()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
task.OnProgramExit("save_favicon_cache", func() {
|
|
||||||
iconCacheMu.Lock()
|
|
||||||
defer iconCacheMu.Unlock()
|
|
||||||
|
|
||||||
if len(iconCache) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := utils.SaveJSON(common.IconCachePath, &iconCache, 0o644); err != nil {
|
|
||||||
logging.Error().Err(err).Msg("failed to save icon cache")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func pruneExpiredIconCache() {
|
|
||||||
iconCacheMu.Lock()
|
|
||||||
defer iconCacheMu.Unlock()
|
|
||||||
|
|
||||||
nPruned := 0
|
|
||||||
for key, icon := range iconCache {
|
|
||||||
if icon.IsExpired() {
|
|
||||||
delete(iconCache, key)
|
|
||||||
nPruned++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if nPruned > 0 {
|
|
||||||
logging.Info().Int("pruned", nPruned).Msg("pruned expired icon cache")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func routeKey(r route.HTTPRoute) string {
|
|
||||||
return r.ProviderName() + ":" + r.TargetName()
|
|
||||||
}
|
|
||||||
|
|
||||||
func PruneRouteIconCache(route route.HTTPRoute) {
|
|
||||||
iconCacheMu.Lock()
|
|
||||||
defer iconCacheMu.Unlock()
|
|
||||||
delete(iconCache, routeKey(route))
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadIconCache(key string) *fetchResult {
|
|
||||||
iconCacheMu.RLock()
|
|
||||||
defer iconCacheMu.RUnlock()
|
|
||||||
|
|
||||||
icon, ok := iconCache[key]
|
|
||||||
if ok && icon != nil {
|
|
||||||
logging.Debug().
|
|
||||||
Str("key", key).
|
|
||||||
Msg("icon found in cache")
|
|
||||||
icon.LastAccess = time.Now()
|
|
||||||
return &fetchResult{icon: icon.Icon}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func storeIconCache(key string, icon []byte) {
|
|
||||||
iconCacheMu.Lock()
|
|
||||||
defer iconCacheMu.Unlock()
|
|
||||||
iconCache[key] = &cacheEntry{Icon: icon, LastAccess: time.Now()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *cacheEntry) IsExpired() bool {
|
|
||||||
return time.Since(e.LastAccess) > iconCacheTTL
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *cacheEntry) UnmarshalJSON(data []byte) error {
|
|
||||||
attempt := struct {
|
|
||||||
Icon []byte `json:"icon"`
|
|
||||||
LastAccess time.Time `json:"lastAccess"`
|
|
||||||
}{}
|
|
||||||
err := json.Unmarshal(data, &attempt)
|
|
||||||
if err == nil {
|
|
||||||
e.Icon = attempt.Icon
|
|
||||||
e.LastAccess = attempt.LastAccess
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// fallback to bytes
|
|
||||||
err = json.Unmarshal(data, &e.Icon)
|
|
||||||
if err == nil {
|
|
||||||
e.LastAccess = time.Now()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,49 +1,11 @@
|
||||||
package favicon
|
package favicon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
|
||||||
"github.com/vincent-petithory/dataurl"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
"github.com/yusing/go-proxy/internal/homepage"
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
|
|
||||||
"github.com/yusing/go-proxy/internal/route/routes"
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
route "github.com/yusing/go-proxy/internal/route/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fetchResult struct {
|
|
||||||
icon []byte
|
|
||||||
contentType string
|
|
||||||
statusCode int
|
|
||||||
errMsg string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (res *fetchResult) OK() bool {
|
|
||||||
return res.icon != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (res *fetchResult) ContentType() string {
|
|
||||||
if res.contentType == "" {
|
|
||||||
if bytes.HasPrefix(res.icon, []byte("<svg")) || bytes.HasPrefix(res.icon, []byte("<?xml")) {
|
|
||||||
return "image/svg+xml"
|
|
||||||
}
|
|
||||||
return "image/x-icon"
|
|
||||||
}
|
|
||||||
return res.contentType
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
MaxRedirectDepth = 5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetFavIcon returns the favicon of the route
|
// GetFavIcon returns the favicon of the route
|
||||||
|
@ -57,11 +19,11 @@ const (
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,216 +31,45 @@ 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 := getFavIconFromURL(&iconURL)
|
fetchResult := homepage.FetchFavIconFromURL(req.Context(), &iconURL)
|
||||||
if !fetchResult.OK() {
|
if !fetchResult.OK() {
|
||||||
http.Error(w, fetchResult.errMsg, fetchResult.statusCode)
|
http.Error(w, fetchResult.ErrMsg, fetchResult.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", fetchResult.ContentType())
|
w.Header().Set("Content-Type", fetchResult.ContentType())
|
||||||
gphttp.WriteBody(w, fetchResult.icon)
|
gphttp.WriteBody(w, fetchResult.Icon)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// try with route.Homepage.Icon
|
// try with route.Icon
|
||||||
r, ok := routes.GetHTTPRoute(alias)
|
r, ok := routes.HTTP.Get(alias)
|
||||||
if !ok {
|
if !ok {
|
||||||
gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound)
|
gphttp.ValueNotFound(w, "route", alias)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var result *fetchResult
|
var result *homepage.FetchResult
|
||||||
hp := r.HomepageItem()
|
hp := r.HomepageItem()
|
||||||
if hp.Icon != nil {
|
if hp.Icon != nil {
|
||||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||||
result = findIcon(r, req, hp.Icon.Value)
|
result = homepage.FindIcon(req.Context(), r, *hp.Icon.FullURL)
|
||||||
} else {
|
} else {
|
||||||
result = getFavIconFromURL(hp.Icon)
|
result = homepage.FetchFavIconFromURL(req.Context(), hp.Icon)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// try extract from "link[rel=icon]"
|
// try extract from "link[rel=icon]"
|
||||||
result = findIcon(r, req, "/")
|
result = homepage.FindIcon(req.Context(), r, "/")
|
||||||
}
|
}
|
||||||
if result.statusCode == 0 {
|
if result.StatusCode == 0 {
|
||||||
result.statusCode = http.StatusOK
|
result.StatusCode = http.StatusOK
|
||||||
}
|
}
|
||||||
if !result.OK() {
|
if !result.OK() {
|
||||||
http.Error(w, result.errMsg, result.statusCode)
|
http.Error(w, result.ErrMsg, result.StatusCode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", result.ContentType())
|
w.Header().Set("Content-Type", result.ContentType())
|
||||||
gphttp.WriteBody(w, result.icon)
|
gphttp.WriteBody(w, result.Icon)
|
||||||
}
|
|
||||||
|
|
||||||
func getFavIconFromURL(iconURL *homepage.IconURL) *fetchResult {
|
|
||||||
switch iconURL.IconSource {
|
|
||||||
case homepage.IconSourceAbsolute:
|
|
||||||
return fetchIconAbsolute(iconURL.URL())
|
|
||||||
case homepage.IconSourceRelative:
|
|
||||||
return &fetchResult{statusCode: http.StatusBadRequest, errMsg: "unexpected relative icon"}
|
|
||||||
case homepage.IconSourceWalkXCode, homepage.IconSourceSelfhSt:
|
|
||||||
return fetchKnownIcon(iconURL)
|
|
||||||
}
|
|
||||||
return &fetchResult{statusCode: http.StatusBadRequest, errMsg: "invalid icon source"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchIconAbsolute(url string) *fetchResult {
|
|
||||||
if result := loadIconCache(url); result != nil {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := gphttp.Get(url)
|
|
||||||
if err != nil || resp.StatusCode != http.StatusOK {
|
|
||||||
if err == nil {
|
|
||||||
err = errors.New(resp.Status)
|
|
||||||
}
|
|
||||||
logging.Error().Err(err).
|
|
||||||
Str("url", url).
|
|
||||||
Msg("failed to get icon")
|
|
||||||
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "connection error"}
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
icon, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
logging.Error().Err(err).
|
|
||||||
Str("url", url).
|
|
||||||
Msg("failed to read icon")
|
|
||||||
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"}
|
|
||||||
}
|
|
||||||
|
|
||||||
storeIconCache(url, icon)
|
|
||||||
return &fetchResult{icon: icon}
|
|
||||||
}
|
|
||||||
|
|
||||||
var nameSanitizer = strings.NewReplacer(
|
|
||||||
"_", "-",
|
|
||||||
" ", "-",
|
|
||||||
"(", "",
|
|
||||||
")", "",
|
|
||||||
)
|
|
||||||
|
|
||||||
func sanitizeName(name string) string {
|
|
||||||
return strings.ToLower(nameSanitizer.Replace(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchKnownIcon(url *homepage.IconURL) *fetchResult {
|
|
||||||
// if icon isn't in the list, no need to fetch
|
|
||||||
if !url.HasIcon() {
|
|
||||||
logging.Debug().
|
|
||||||
Str("value", url.String()).
|
|
||||||
Str("url", url.URL()).
|
|
||||||
Msg("no such icon")
|
|
||||||
return &fetchResult{statusCode: http.StatusNotFound, errMsg: "no such icon"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetchIconAbsolute(url.URL())
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchIcon(filetype, filename string) *fetchResult {
|
|
||||||
result := fetchKnownIcon(homepage.NewSelfhStIconURL(filename, filetype))
|
|
||||||
if result.icon == nil {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
return fetchKnownIcon(homepage.NewWalkXCodeIconURL(filename, filetype))
|
|
||||||
}
|
|
||||||
|
|
||||||
func findIcon(r route.HTTPRoute, req *http.Request, uri string) *fetchResult {
|
|
||||||
key := routeKey(r)
|
|
||||||
if result := loadIconCache(key); result != nil {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
result := fetchIcon("png", sanitizeName(r.TargetName()))
|
|
||||||
cont := r.ContainerInfo()
|
|
||||||
if !result.OK() && cont != nil {
|
|
||||||
result = fetchIcon("png", sanitizeName(cont.Image.Name))
|
|
||||||
}
|
|
||||||
if !result.OK() {
|
|
||||||
// fallback to parse html
|
|
||||||
result = findIconSlow(r, req, uri, 0)
|
|
||||||
}
|
|
||||||
if result.OK() {
|
|
||||||
storeIconCache(key, result.icon)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func findIconSlow(r route.HTTPRoute, req *http.Request, uri string, depth int) *fetchResult {
|
|
||||||
ctx, cancel := context.WithTimeoutCause(req.Context(), 3*time.Second, errors.New("favicon request timeout"))
|
|
||||||
defer cancel()
|
|
||||||
newReq := req.WithContext(ctx)
|
|
||||||
newReq.Header.Set("Accept-Encoding", "identity") // disable compression
|
|
||||||
u, err := url.ParseRequestURI(strutils.SanitizeURI(uri))
|
|
||||||
if err != nil {
|
|
||||||
logging.Error().Err(err).
|
|
||||||
Str("route", r.TargetName()).
|
|
||||||
Str("path", uri).
|
|
||||||
Msg("failed to parse uri")
|
|
||||||
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "cannot parse uri"}
|
|
||||||
}
|
|
||||||
newReq.URL.Path = u.Path
|
|
||||||
newReq.URL.RawPath = u.RawPath
|
|
||||||
newReq.URL.RawQuery = u.RawQuery
|
|
||||||
newReq.RequestURI = u.String()
|
|
||||||
|
|
||||||
c := newContent()
|
|
||||||
r.ServeHTTP(c, newReq)
|
|
||||||
if c.status != http.StatusOK {
|
|
||||||
switch c.status {
|
|
||||||
case 0:
|
|
||||||
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "connection error"}
|
|
||||||
default:
|
|
||||||
if loc := c.Header().Get("Location"); loc != "" {
|
|
||||||
if depth > MaxRedirectDepth {
|
|
||||||
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "too many redirects"}
|
|
||||||
}
|
|
||||||
loc = strutils.SanitizeURI(loc)
|
|
||||||
if loc == "/" || loc == newReq.URL.Path {
|
|
||||||
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "circular redirect"}
|
|
||||||
}
|
|
||||||
return findIconSlow(r, req, loc, depth+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &fetchResult{statusCode: c.status, errMsg: "upstream error: " + string(c.data)}
|
|
||||||
}
|
|
||||||
// return icon data
|
|
||||||
if !gphttp.GetContentType(c.header).IsHTML() {
|
|
||||||
return &fetchResult{icon: c.data, contentType: c.header.Get("Content-Type")}
|
|
||||||
}
|
|
||||||
// try extract from "link[rel=icon]" from path "/"
|
|
||||||
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(c.data))
|
|
||||||
if err != nil {
|
|
||||||
logging.Error().Err(err).
|
|
||||||
Str("route", r.TargetName()).
|
|
||||||
Msg("failed to parse html")
|
|
||||||
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"}
|
|
||||||
}
|
|
||||||
ele := doc.Find("head > link[rel=icon]").First()
|
|
||||||
if ele.Length() == 0 {
|
|
||||||
return &fetchResult{statusCode: http.StatusNotFound, errMsg: "icon element not found"}
|
|
||||||
}
|
|
||||||
href := ele.AttrOr("href", "")
|
|
||||||
if href == "" {
|
|
||||||
return &fetchResult{statusCode: http.StatusNotFound, errMsg: "icon href not found"}
|
|
||||||
}
|
|
||||||
// https://en.wikipedia.org/wiki/Data_URI_scheme
|
|
||||||
if strings.HasPrefix(href, "data:image/") {
|
|
||||||
dataURI, err := dataurl.DecodeString(href)
|
|
||||||
if err != nil {
|
|
||||||
logging.Error().Err(err).
|
|
||||||
Str("route", r.TargetName()).
|
|
||||||
Msg("failed to decode favicon")
|
|
||||||
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"}
|
|
||||||
}
|
|
||||||
return &fetchResult{icon: dataURI.Data, contentType: dataURI.ContentType()}
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(href, "http://"), strings.HasPrefix(href, "https://"):
|
|
||||||
return fetchIconAbsolute(href)
|
|
||||||
default:
|
|
||||||
return findIconSlow(r, req, href, 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,15 +9,15 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Health(w http.ResponseWriter, r *http.Request) {
|
func Health(w http.ResponseWriter, r *http.Request) {
|
||||||
if httpheaders.IsWebsocket(r.Header) {
|
if httpheaders.IsWebsocket(r.Header) {
|
||||||
gpwebsocket.Periodic(w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
gpwebsocket.Periodic(w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
||||||
return wsjson.Write(r.Context(), conn, routequery.HealthMap())
|
return wsjson.Write(r.Context(), conn, routes.HealthMap())
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
gphttp.RespondJSON(w, r, routequery.HealthMap())
|
gphttp.RespondJSON(w, r, routes.HealthMap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/routequery"
|
|
||||||
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, routequery.RoutesByAlias(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, routequery.HomepageConfig(r.FormValue("category"), r.FormValue("provider")))
|
|
||||||
case ListRouteProviders:
|
|
||||||
gphttp.RespondJSON(w, r, cfg.RouteProviderList())
|
|
||||||
case ListHomepageCategories:
|
|
||||||
gphttp.RespondJSON(w, r, routequery.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 routequery.RoutesByAlias()
|
|
||||||
}
|
|
||||||
routes := routequery.RoutesByAlias()
|
|
||||||
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)
|
|
||||||
}
|
|
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)
|
||||||
|
}
|
||||||
|
}
|
23
internal/api/v1/list_route_providers.go
Normal file
23
internal/api/v1/list_route_providers.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
"github.com/coder/websocket/wsjson"
|
||||||
|
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 wsjson.Write(r.Context(), conn, 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())
|
||||||
|
}
|
|
@ -14,34 +14,33 @@ import (
|
||||||
"github.com/yusing/go-proxy/agent/pkg/certs"
|
"github.com/yusing/go-proxy/agent/pkg/certs"
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAgent(w http.ResponseWriter, r *http.Request) {
|
func NewAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
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 := config.GetInstance().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")
|
||||||
|
@ -49,14 +48,14 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
nightly := strutils.ParseBool(q.Get("nightly"))
|
nightly, _ := strconv.ParseBool(q.Get("nightly"))
|
||||||
var image string
|
var image string
|
||||||
if nightly {
|
if nightly {
|
||||||
image = agent.DockerImageNightly
|
image = agent.DockerImageNightly
|
||||||
|
@ -110,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,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)
|
|
||||||
}
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -40,7 +41,7 @@ func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
gphttp.WriteBody(w, respData)
|
gphttp.WriteBody(w, respData)
|
||||||
} else {
|
} else {
|
||||||
rp := reverseproxy.NewReverseProxy("agent", agentPkg.AgentURL, agent.Transport())
|
rp := reverseproxy.NewReverseProxy("agent", types.NewURL(agentPkg.AgentURL), agent.Transport())
|
||||||
header := r.Header.Clone()
|
header := r.Header.Clone()
|
||||||
r, err := http.NewRequestWithContext(r.Context(), r.Method, agentPkg.EndpointSystemInfo+"?"+query.Encode(), nil)
|
r, err := http.NewRequestWithContext(r.Context(), r.Method, agentPkg.EndpointSystemInfo+"?"+query.Encode(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
package v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
|
||||||
"github.com/yusing/go-proxy/pkg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetVersion(w http.ResponseWriter, r *http.Request) {
|
|
||||||
gphttp.WriteBody(w, []byte(pkg.GetVersion()))
|
|
||||||
}
|
|
|
@ -38,15 +38,36 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ProceedNext(w http.ResponseWriter, r *http.Request) {
|
||||||
|
next, ok := r.Context().Value(nextHandlerContextKey).(http.HandlerFunc)
|
||||||
|
if ok {
|
||||||
|
next(w, r)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := defaultAuth.CheckToken(r); err != nil {
|
||||||
|
defaultAuth.LoginHandler(w, r)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
return next
|
|
||||||
}
|
}
|
221
internal/auth/oauth_refresh.go
Normal file
221
internal/auth/oauth_refresh.go
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/jsonstore"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type oauthRefreshToken struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
Expiry time.Time `json:"expiry"`
|
||||||
|
|
||||||
|
result *RefreshResult
|
||||||
|
err error
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
SessionID sessionID `json:"session_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshResult struct {
|
||||||
|
newSession Session
|
||||||
|
jwt string
|
||||||
|
jwtExpiry time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionClaims struct {
|
||||||
|
Session
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionID string
|
||||||
|
|
||||||
|
var oauthRefreshTokens jsonstore.MapStore[*oauthRefreshToken]
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultRefreshTokenExpiry = 30 * 24 * time.Hour // 1 month
|
||||||
|
sessionInvalidateDelay = 3 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNoRefreshToken = errors.New("no refresh token")
|
||||||
|
ErrRefreshTokenFailure = errors.New("failed to refresh token")
|
||||||
|
)
|
||||||
|
|
||||||
|
const sessionTokenIssuer = "GoDoxy"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if IsOIDCEnabled() {
|
||||||
|
oauthRefreshTokens = jsonstore.Store[*oauthRefreshToken]("oauth_refresh_tokens")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (token *oauthRefreshToken) expired() bool {
|
||||||
|
return time.Now().After(token.Expiry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSessionID() sessionID {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
_, _ = rand.Read(b)
|
||||||
|
return sessionID(hex.EncodeToString(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSession(username string, groups []string) Session {
|
||||||
|
return Session{
|
||||||
|
SessionID: newSessionID(),
|
||||||
|
Username: username,
|
||||||
|
Groups: groups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOAuthRefreshToken returns the refresh token for the given session.
|
||||||
|
func getOAuthRefreshToken(claims *Session) (*oauthRefreshToken, bool) {
|
||||||
|
token, ok := oauthRefreshTokens.Load(string(claims.SessionID))
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.expired() {
|
||||||
|
invalidateOAuthRefreshToken(claims.SessionID)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Username != token.Username {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return token, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeOAuthRefreshToken(sessionID sessionID, username, token string) {
|
||||||
|
oauthRefreshTokens.Store(string(sessionID), &oauthRefreshToken{
|
||||||
|
Username: username,
|
||||||
|
RefreshToken: token,
|
||||||
|
Expiry: time.Now().Add(defaultRefreshTokenExpiry),
|
||||||
|
})
|
||||||
|
logging.Debug().Str("username", username).Msg("stored oauth refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidateOAuthRefreshToken(sessionID sessionID) {
|
||||||
|
logging.Debug().Str("session_id", string(sessionID)).Msg("invalidating oauth refresh token")
|
||||||
|
oauthRefreshTokens.Delete(string(sessionID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) setSessionTokenCookie(w http.ResponseWriter, r *http.Request, session Session) {
|
||||||
|
claims := &sessionClaims{
|
||||||
|
Session: session,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
Issuer: sessionTokenIssuer,
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(common.APIJWTTokenTTL)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
||||||
|
signed, err := jwtToken.SignedString(common.APIJWTSecret)
|
||||||
|
if err != nil {
|
||||||
|
logging.Err(err).Msg("failed to sign session token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
SetTokenCookie(w, r, CookieOauthSessionToken, signed, common.APIJWTTokenTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionClaims, valid bool, err error) {
|
||||||
|
claims = &sessionClaims{}
|
||||||
|
sessionToken, err := jwt.ParseWithClaims(sessionJWT, claims, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
|
}
|
||||||
|
return common.APIJWTSecret, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return claims, sessionToken.Valid && claims.Issuer == sessionTokenIssuer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) TryRefreshToken(ctx context.Context, sessionJWT string) (*RefreshResult, error) {
|
||||||
|
// verify the session cookie
|
||||||
|
claims, valid, err := auth.parseSessionJWT(sessionJWT)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrInvalidSessionToken, err)
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return nil, ErrInvalidSessionToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if refresh is possible
|
||||||
|
refreshToken, ok := getOAuthRefreshToken(&claims.Session)
|
||||||
|
if !ok {
|
||||||
|
return nil, errNoRefreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if !auth.checkAllowed(claims.Username, claims.Groups) {
|
||||||
|
return nil, ErrUserNotAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth.doRefreshToken(ctx, refreshToken, &claims.Session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) doRefreshToken(ctx context.Context, refreshToken *oauthRefreshToken, claims *Session) (*RefreshResult, error) {
|
||||||
|
refreshToken.mu.Lock()
|
||||||
|
defer refreshToken.mu.Unlock()
|
||||||
|
|
||||||
|
// already refreshed
|
||||||
|
// this must be called after refresh but before invalidate
|
||||||
|
if refreshToken.result != nil || refreshToken.err != nil {
|
||||||
|
return refreshToken.result, refreshToken.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// this step refreshes the token
|
||||||
|
// see https://cs.opensource.google/go/x/oauth2/+/refs/tags/v0.29.0:oauth2.go;l=313
|
||||||
|
newToken, err := auth.oauthConfig.TokenSource(ctx, &oauth2.Token{
|
||||||
|
RefreshToken: refreshToken.RefreshToken,
|
||||||
|
}).Token()
|
||||||
|
if err != nil {
|
||||||
|
refreshToken.err = fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrRefreshTokenFailure, err)
|
||||||
|
return nil, refreshToken.err
|
||||||
|
}
|
||||||
|
|
||||||
|
idTokenJWT, idToken, err := auth.getIdToken(ctx, newToken)
|
||||||
|
if err != nil {
|
||||||
|
refreshToken.err = fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrRefreshTokenFailure, err)
|
||||||
|
return nil, refreshToken.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case there're multiple requests for the same session to refresh
|
||||||
|
// invalidate the token after a short delay
|
||||||
|
go func() {
|
||||||
|
<-time.After(sessionInvalidateDelay)
|
||||||
|
invalidateOAuthRefreshToken(claims.SessionID)
|
||||||
|
}()
|
||||||
|
|
||||||
|
sessionID := newSessionID()
|
||||||
|
|
||||||
|
logging.Debug().Str("username", claims.Username).Time("expiry", newToken.Expiry).Msg("refreshed token")
|
||||||
|
storeOAuthRefreshToken(sessionID, claims.Username, newToken.RefreshToken)
|
||||||
|
|
||||||
|
refreshToken.result = &RefreshResult{
|
||||||
|
newSession: Session{
|
||||||
|
SessionID: sessionID,
|
||||||
|
Username: claims.Username,
|
||||||
|
Groups: claims.Groups,
|
||||||
|
},
|
||||||
|
jwt: idTokenJWT,
|
||||||
|
jwtExpiry: idToken.Expiry,
|
||||||
|
}
|
||||||
|
return refreshToken.result, nil
|
||||||
|
}
|
336
internal/auth/oidc.go
Normal file
336
internal/auth/oidc.go
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
OIDCProvider struct {
|
||||||
|
oauthConfig *oauth2.Config
|
||||||
|
oidcProvider *oidc.Provider
|
||||||
|
oidcVerifier *oidc.IDTokenVerifier
|
||||||
|
endSessionURL *url.URL
|
||||||
|
allowedUsers []string
|
||||||
|
allowedGroups []string
|
||||||
|
}
|
||||||
|
|
||||||
|
IDTokenClaims struct {
|
||||||
|
Username string `json:"preferred_username"`
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CookieOauthState = "godoxy_oidc_state"
|
||||||
|
CookieOauthToken = "godoxy_oauth_token"
|
||||||
|
CookieOauthSessionToken = "godoxy_session_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OIDCAuthInitPath = "/"
|
||||||
|
OIDCPostAuthPath = "/auth/callback"
|
||||||
|
OIDCLogoutPath = "/auth/logout"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errMissingIDToken = errors.New("missing id_token field from oauth token")
|
||||||
|
|
||||||
|
ErrMissingOAuthToken = gperr.New("missing oauth token")
|
||||||
|
ErrInvalidOAuthToken = gperr.New("invalid oauth token")
|
||||||
|
)
|
||||||
|
|
||||||
|
// generateState generates a random string for OIDC state.
|
||||||
|
const oidcStateLength = 32
|
||||||
|
|
||||||
|
func generateState() string {
|
||||||
|
b := make([]byte, oidcStateLength)
|
||||||
|
_, _ = rand.Read(b)
|
||||||
|
return base64.URLEncoding.EncodeToString(b)[:oidcStateLength]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, allowedGroups []string) (*OIDCProvider, error) {
|
||||||
|
if len(allowedUsers)+len(allowedGroups) == 0 {
|
||||||
|
return nil, errors.New("oidc.allowed_users or oidc.allowed_groups are both empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
provider, err := oidc.NewProvider(ctx, issuerURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endSessionURL, err := url.Parse(provider.EndSessionEndpoint())
|
||||||
|
if err != nil && provider.EndSessionEndpoint() != "" {
|
||||||
|
// non critical, just warn
|
||||||
|
logging.Warn().
|
||||||
|
Str("issuer", issuerURL).
|
||||||
|
Err(err).
|
||||||
|
Msg("failed to parse end session URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OIDCProvider{
|
||||||
|
oauthConfig: &oauth2.Config{
|
||||||
|
ClientID: clientID,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
RedirectURL: "",
|
||||||
|
Endpoint: provider.Endpoint(),
|
||||||
|
Scopes: common.OIDCScopes,
|
||||||
|
},
|
||||||
|
oidcProvider: provider,
|
||||||
|
oidcVerifier: provider.Verifier(&oidc.Config{
|
||||||
|
ClientID: clientID,
|
||||||
|
}),
|
||||||
|
endSessionURL: endSessionURL,
|
||||||
|
allowedUsers: allowedUsers,
|
||||||
|
allowedGroups: allowedGroups,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOIDCProviderFromEnv creates a new OIDCProvider from environment variables.
|
||||||
|
func NewOIDCProviderFromEnv() (*OIDCProvider, error) {
|
||||||
|
return NewOIDCProvider(
|
||||||
|
common.OIDCIssuerURL,
|
||||||
|
common.OIDCClientID,
|
||||||
|
common.OIDCClientSecret,
|
||||||
|
common.OIDCAllowedUsers,
|
||||||
|
common.OIDCAllowedGroups,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) SetAllowedUsers(users []string) {
|
||||||
|
auth.allowedUsers = users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) SetAllowedGroups(groups []string) {
|
||||||
|
auth.allowedGroups = groups
|
||||||
|
}
|
||||||
|
|
||||||
|
// optRedirectPostAuth returns an oauth2 option that sets the "redirect_uri"
|
||||||
|
// parameter of the authorization URL to the post auth path of the current
|
||||||
|
// request host.
|
||||||
|
func optRedirectPostAuth(r *http.Request) oauth2.AuthCodeOption {
|
||||||
|
return oauth2.SetAuthURLParam("redirect_uri", "https://"+requestHost(r)+OIDCPostAuthPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) getIdToken(ctx context.Context, oauthToken *oauth2.Token) (string, *oidc.IDToken, error) {
|
||||||
|
idTokenJWT, ok := oauthToken.Extra("id_token").(string)
|
||||||
|
if !ok {
|
||||||
|
return "", nil, errMissingIDToken
|
||||||
|
}
|
||||||
|
idToken, err := auth.oidcVerifier.Verify(ctx, idTokenJWT)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("failed to verify ID token: %w", err)
|
||||||
|
}
|
||||||
|
return idTokenJWT, idToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
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 {
|
||||||
|
case OIDCAuthInitPath:
|
||||||
|
auth.LoginHandler(w, r)
|
||||||
|
case OIDCPostAuthPath:
|
||||||
|
auth.PostAuthCallbackHandler(w, r)
|
||||||
|
case OIDCLogoutPath:
|
||||||
|
auth.LogoutHandler(w, r)
|
||||||
|
default:
|
||||||
|
http.Redirect(w, r, OIDCAuthInitPath, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// check for session token
|
||||||
|
sessionToken, err := r.Cookie(CookieOauthSessionToken)
|
||||||
|
if err == nil { // session token exists
|
||||||
|
result, err := auth.TryRefreshToken(r.Context(), sessionToken.Value)
|
||||||
|
// redirect back to where they requested
|
||||||
|
// when token refresh is ok
|
||||||
|
if err == nil {
|
||||||
|
auth.setIDTokenCookie(w, r, result.jwt, time.Until(result.jwtExpiry))
|
||||||
|
auth.setSessionTokenCookie(w, r, result.newSession)
|
||||||
|
ProceedNext(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// clear cookies then redirect to home
|
||||||
|
logging.Err(err).Msg("failed to refresh token")
|
||||||
|
auth.clearCookie(w, r)
|
||||||
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := generateState()
|
||||||
|
SetTokenCookie(w, r, CookieOauthState, state, 300*time.Second)
|
||||||
|
// redirect user to Idp
|
||||||
|
http.Redirect(w, r, auth.oauthConfig.AuthCodeURL(state, optRedirectPostAuth(r)), http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseClaims(idToken *oidc.IDToken) (*IDTokenClaims, error) {
|
||||||
|
var claim IDTokenClaims
|
||||||
|
if err := idToken.Claims(&claim); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse claims: %w", err)
|
||||||
|
}
|
||||||
|
if claim.Username == "" {
|
||||||
|
return nil, errors.New("missing username in ID token")
|
||||||
|
}
|
||||||
|
return &claim, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) checkAllowed(user string, groups []string) bool {
|
||||||
|
userAllowed := slices.Contains(auth.allowedUsers, user)
|
||||||
|
if !userAllowed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(auth.allowedGroups) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return len(utils.Intersect(groups, auth.allowedGroups)) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) CheckToken(r *http.Request) error {
|
||||||
|
tokenCookie, err := r.Cookie(CookieOauthToken)
|
||||||
|
if err != nil {
|
||||||
|
return ErrMissingOAuthToken
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, err := auth.oidcVerifier.Verify(r.Context(), tokenCookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrInvalidOAuthToken, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := parseClaims(idToken)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrInvalidOAuthToken, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !auth.checkAllowed(claims.Username, claims.Groups) {
|
||||||
|
return ErrUserNotAllowed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// For testing purposes, skip provider verification
|
||||||
|
if common.IsTest {
|
||||||
|
auth.handleTestCallback(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify state
|
||||||
|
state, err := r.Cookie(CookieOauthState)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.BadRequest(w, "missing state cookie")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.URL.Query().Get("state") != state.Value {
|
||||||
|
gphttp.BadRequest(w, "invalid oauth state")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code := r.URL.Query().Get("code")
|
||||||
|
oauth2Token, err := auth.oauthConfig.Exchange(r.Context(), code, optRedirectPostAuth(r))
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ServerError(w, r, fmt.Errorf("failed to exchange token: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idTokenJWT, idToken, err := auth.getIdToken(r.Context(), oauth2Token)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if oauth2Token.RefreshToken != "" {
|
||||||
|
claims, err := parseClaims(idToken)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session := newSession(claims.Username, claims.Groups)
|
||||||
|
storeOAuthRefreshToken(session.SessionID, claims.Username, oauth2Token.RefreshToken)
|
||||||
|
auth.setSessionTokenCookie(w, r, session)
|
||||||
|
}
|
||||||
|
auth.setIDTokenCookie(w, r, idTokenJWT, time.Until(idToken.Expiry))
|
||||||
|
|
||||||
|
// Redirect to home page
|
||||||
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
oauthToken, _ := r.Cookie(CookieOauthToken)
|
||||||
|
sessionToken, _ := r.Cookie(CookieOauthSessionToken)
|
||||||
|
auth.clearCookie(w, r)
|
||||||
|
|
||||||
|
if sessionToken != nil {
|
||||||
|
claims, _, err := auth.parseSessionJWT(sessionToken.Value)
|
||||||
|
if err == nil {
|
||||||
|
invalidateOAuthRefreshToken(claims.SessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "/"
|
||||||
|
if auth.endSessionURL != nil && oauthToken != nil {
|
||||||
|
query := auth.endSessionURL.Query()
|
||||||
|
query.Set("id_token_hint", oauthToken.Value)
|
||||||
|
query.Set("post_logout_redirect_uri", "https://"+requestHost(r))
|
||||||
|
|
||||||
|
clone := *auth.endSessionURL
|
||||||
|
clone.RawQuery = query.Encode()
|
||||||
|
url = clone.String()
|
||||||
|
} else if auth.endSessionURL != nil {
|
||||||
|
url = auth.endSessionURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, url, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) setIDTokenCookie(w http.ResponseWriter, r *http.Request, jwt string, ttl time.Duration) {
|
||||||
|
SetTokenCookie(w, r, CookieOauthToken, jwt, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) clearCookie(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ClearTokenCookie(w, r, CookieOauthToken)
|
||||||
|
ClearTokenCookie(w, r, CookieOauthSessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTestCallback handles OIDC callback in test environment.
|
||||||
|
func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
|
state, err := r.Cookie(CookieOauthState)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.BadRequest(w, "missing state cookie")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Query().Get("state") != state.Value {
|
||||||
|
gphttp.BadRequest(w, "invalid oauth state")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test JWT token
|
||||||
|
SetTokenCookie(w, r, CookieOauthToken, "test", time.Hour)
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -23,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",
|
||||||
|
@ -35,6 +35,7 @@ func setupMockOIDC(t *testing.T) {
|
||||||
},
|
},
|
||||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||||
},
|
},
|
||||||
|
endSessionURL: Must(url.Parse("http://mock-provider/logout")),
|
||||||
oidcProvider: provider,
|
oidcProvider: provider,
|
||||||
oidcVerifier: provider.Verifier(&oidc.Config{
|
oidcVerifier: provider.Verifier(&oidc.Config{
|
||||||
ClientID: "test-client",
|
ClientID: "test-client",
|
||||||
|
@ -102,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{
|
||||||
|
@ -148,17 +149,17 @@ func TestOIDCLoginHandler(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Success - Redirects to provider",
|
name: "Success - Redirects to provider",
|
||||||
wantStatus: http.StatusTemporaryRedirect,
|
wantStatus: http.StatusFound,
|
||||||
wantRedirect: true,
|
wantRedirect: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodGet, "/auth/redirect", nil)
|
req := httptest.NewRequest(http.MethodGet, OIDCAuthInitPath, nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
defaultAuth.RedirectLoginPage(w, req)
|
defaultAuth.(*OIDCProvider).HandleAuth(w, req)
|
||||||
|
|
||||||
if got := w.Code; got != tt.wantStatus {
|
if got := w.Code; got != tt.wantStatus {
|
||||||
t.Errorf("OIDCLoginHandler() status = %v, want %v", got, tt.wantStatus)
|
t.Errorf("OIDCLoginHandler() status = %v, want %v", got, tt.wantStatus)
|
||||||
|
@ -194,7 +195,7 @@ func TestOIDCCallbackHandler(t *testing.T) {
|
||||||
state: "valid-state",
|
state: "valid-state",
|
||||||
code: "valid-code",
|
code: "valid-code",
|
||||||
setupMocks: true,
|
setupMocks: true,
|
||||||
wantStatus: http.StatusTemporaryRedirect,
|
wantStatus: http.StatusFound,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Failure - Missing state",
|
name: "Failure - Missing state",
|
||||||
|
@ -219,7 +220,7 @@ func TestOIDCCallbackHandler(t *testing.T) {
|
||||||
}
|
}
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
defaultAuth.LoginCallbackHandler(w, req)
|
defaultAuth.(*OIDCProvider).PostAuthCallbackHandler(w, req)
|
||||||
|
|
||||||
if got := w.Code; got != tt.wantStatus {
|
if got := w.Code; got != tt.wantStatus {
|
||||||
t.Errorf("OIDCCallbackHandler() status = %v, want %v", got, tt.wantStatus)
|
t.Errorf("OIDCCallbackHandler() status = %v, want %v", got, tt.wantStatus)
|
||||||
|
@ -227,7 +228,7 @@ func TestOIDCCallbackHandler(t *testing.T) {
|
||||||
|
|
||||||
if tt.wantStatus == http.StatusTemporaryRedirect {
|
if tt.wantStatus == http.StatusTemporaryRedirect {
|
||||||
setCookie := Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
setCookie := Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
||||||
ExpectEqual(t, setCookie.Name, defaultAuth.TokenCookieName())
|
ExpectEqual(t, setCookie.Name, CookieOauthToken)
|
||||||
ExpectTrue(t, setCookie.Value != "")
|
ExpectTrue(t, setCookie.Value != "")
|
||||||
ExpectEqual(t, setCookie.Path, "/")
|
ExpectEqual(t, setCookie.Path, "/")
|
||||||
ExpectEqual(t, setCookie.SameSite, http.SameSiteLaxMode)
|
ExpectEqual(t, setCookie.SameSite, http.SameSiteLaxMode)
|
||||||
|
@ -270,7 +271,6 @@ func TestInitOIDC(t *testing.T) {
|
||||||
issuerURL: server.URL,
|
issuerURL: server.URL,
|
||||||
clientID: "client_id",
|
clientID: "client_id",
|
||||||
clientSecret: "client_secret",
|
clientSecret: "client_secret",
|
||||||
redirectURL: "https://example.com/callback",
|
|
||||||
allowedUsers: []string{"user1", "user2"},
|
allowedUsers: []string{"user1", "user2"},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
@ -279,7 +279,6 @@ func TestInitOIDC(t *testing.T) {
|
||||||
issuerURL: server.URL,
|
issuerURL: server.URL,
|
||||||
clientID: "client_id",
|
clientID: "client_id",
|
||||||
clientSecret: "client_secret",
|
clientSecret: "client_secret",
|
||||||
redirectURL: "https://example.com/callback",
|
|
||||||
allowedGroups: []string{"group1", "group2"},
|
allowedGroups: []string{"group1", "group2"},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
@ -288,7 +287,6 @@ func TestInitOIDC(t *testing.T) {
|
||||||
issuerURL: server.URL,
|
issuerURL: server.URL,
|
||||||
clientID: "client_id",
|
clientID: "client_id",
|
||||||
clientSecret: "client_secret",
|
clientSecret: "client_secret",
|
||||||
redirectURL: "https://example.com/callback",
|
|
||||||
logoutURL: "https://example.com/logout",
|
logoutURL: "https://example.com/logout",
|
||||||
allowedUsers: []string{"user1", "user2"},
|
allowedUsers: []string{"user1", "user2"},
|
||||||
allowedGroups: []string{"group1", "group2"},
|
allowedGroups: []string{"group1", "group2"},
|
||||||
|
@ -299,14 +297,13 @@ func TestInitOIDC(t *testing.T) {
|
||||||
issuerURL: "https://example.com",
|
issuerURL: "https://example.com",
|
||||||
clientID: "client_id",
|
clientID: "client_id",
|
||||||
clientSecret: "client_secret",
|
clientSecret: "client_secret",
|
||||||
redirectURL: "https://example.com/callback",
|
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
_, err := NewOIDCProvider(tt.issuerURL, tt.clientID, tt.clientSecret, tt.redirectURL, tt.allowedUsers, tt.allowedGroups)
|
_, err := NewOIDCProvider(tt.issuerURL, tt.clientID, tt.clientSecret, tt.allowedUsers, tt.allowedGroups)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("InitOIDC() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("InitOIDC() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
}
|
}
|
||||||
|
@ -400,7 +397,7 @@ func TestCheckToken(t *testing.T) {
|
||||||
"preferred_username": "user1",
|
"preferred_username": "user1",
|
||||||
"groups": []string{"group1"},
|
"groups": []string{"group1"},
|
||||||
},
|
},
|
||||||
wantErr: ErrInvalidToken,
|
wantErr: ErrInvalidOAuthToken,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Error - Server returns incorrect audience",
|
name: "Error - Server returns incorrect audience",
|
||||||
|
@ -411,7 +408,7 @@ func TestCheckToken(t *testing.T) {
|
||||||
"preferred_username": "user1",
|
"preferred_username": "user1",
|
||||||
"groups": []string{"group1"},
|
"groups": []string{"group1"},
|
||||||
},
|
},
|
||||||
wantErr: ErrInvalidToken,
|
wantErr: ErrInvalidOAuthToken,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Error - Server returns expired token",
|
name: "Error - Server returns expired token",
|
||||||
|
@ -422,7 +419,7 @@ func TestCheckToken(t *testing.T) {
|
||||||
"preferred_username": "user1",
|
"preferred_username": "user1",
|
||||||
"groups": []string{"group1"},
|
"groups": []string{"group1"},
|
||||||
},
|
},
|
||||||
wantErr: ErrInvalidToken,
|
wantErr: ErrInvalidOAuthToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
@ -438,7 +435,7 @@ func TestCheckToken(t *testing.T) {
|
||||||
// Craft a test HTTP request that includes the token as a cookie.
|
// Craft a test HTTP request that includes the token as a cookie.
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
req.AddCookie(&http.Cookie{
|
req.AddCookie(&http.Cookie{
|
||||||
Name: auth.TokenCookieName(),
|
Name: CookieOauthToken,
|
||||||
Value: signedToken,
|
Value: signedToken,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -452,3 +449,35 @@ func TestCheckToken(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLogoutHandler(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
setupMockOIDC(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, OIDCLogoutPath, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
req.AddCookie(&http.Cookie{
|
||||||
|
Name: CookieOauthToken,
|
||||||
|
Value: "test-token",
|
||||||
|
})
|
||||||
|
req.AddCookie(&http.Cookie{
|
||||||
|
Name: CookieOauthSessionToken,
|
||||||
|
Value: "test-session-token",
|
||||||
|
})
|
||||||
|
|
||||||
|
defaultAuth.(*OIDCProvider).LogoutHandler(w, req)
|
||||||
|
|
||||||
|
if got := w.Code; got != http.StatusFound {
|
||||||
|
t.Errorf("LogoutHandler() status = %v, want %v", got, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := w.Header().Get("Location"); got == "" {
|
||||||
|
t.Error("LogoutHandler() missing redirect location")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(w.Header().Values("Set-Cookie")) != 2 {
|
||||||
|
t.Error("LogoutHandler() did not clear all cookies")
|
||||||
|
}
|
||||||
|
}
|
10
internal/auth/provider.go
Normal file
10
internal/auth/provider.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
CheckToken(r *http.Request) error
|
||||||
|
LoginHandler(w http.ResponseWriter, r *http.Request)
|
||||||
|
PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request)
|
||||||
|
LogoutHandler(w http.ResponseWriter, r *http.Request)
|
||||||
|
}
|
|
@ -76,7 +76,7 @@ func (auth *UserPassAuth) NewToken() (token string, err error) {
|
||||||
func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
||||||
jwtCookie, err := r.Cookie(auth.TokenCookieName())
|
jwtCookie, err := r.Cookie(auth.TokenCookieName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrMissingToken
|
return ErrMissingSessionToken
|
||||||
}
|
}
|
||||||
var claims UserPassClaims
|
var claims UserPassClaims
|
||||||
token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (interface{}, error) {
|
token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
@ -90,7 +90,7 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case !token.Valid:
|
case !token.Valid:
|
||||||
return ErrInvalidToken
|
return ErrInvalidSessionToken
|
||||||
case claims.Username != auth.username:
|
case claims.Username != auth.username:
|
||||||
return ErrUserNotAllowed.Subject(claims.Username)
|
return ErrUserNotAllowed.Subject(claims.Username)
|
||||||
case claims.ExpiresAt.Before(time.Now()):
|
case claims.ExpiresAt.Before(time.Now()):
|
||||||
|
@ -100,11 +100,7 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *UserPassAuth) RedirectLoginPage(w http.ResponseWriter, r *http.Request) {
|
func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *UserPassAuth) LoginCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var creds struct {
|
var creds struct {
|
||||||
User string `json:"username"`
|
User string `json:"username"`
|
||||||
Pass string `json:"password"`
|
Pass string `json:"password"`
|
||||||
|
@ -123,12 +119,17 @@ func (auth *UserPassAuth) LoginCallbackHandler(w http.ResponseWriter, r *http.Re
|
||||||
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) LogoutCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
func (auth *UserPassAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
DefaultLogoutCallbackHandler(auth, w, r)
|
http.Redirect(w, r, "/login", http.StatusFound) // redirects to WebUI login page
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ClearTokenCookie(w, r, auth.TokenCookieName())
|
||||||
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *UserPassAuth) validatePassword(user, pass string) error {
|
func (auth *UserPassAuth) validatePassword(user, pass string) error {
|
|
@ -98,7 +98,7 @@ func TestUserPassLoginCallbackHandler(t *testing.T) {
|
||||||
Host: "app.example.com",
|
Host: "app.example.com",
|
||||||
Body: io.NopCloser(bytes.NewReader(Must(json.Marshal(tt.creds)))),
|
Body: io.NopCloser(bytes.NewReader(Must(json.Marshal(tt.creds)))),
|
||||||
}
|
}
|
||||||
auth.LoginCallbackHandler(w, req)
|
auth.PostAuthCallbackHandler(w, req)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
ExpectEqual(t, w.Code, http.StatusUnauthorized)
|
ExpectEqual(t, w.Code, http.StatusUnauthorized)
|
||||||
} else {
|
} else {
|
71
internal/auth/utils.go
Normal file
71
internal/auth/utils.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrMissingSessionToken = gperr.New("missing session token")
|
||||||
|
ErrInvalidSessionToken = gperr.New("invalid session token")
|
||||||
|
ErrUserNotAllowed = gperr.New("user not allowed")
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsFrontend(r *http.Request) bool {
|
||||||
|
return r.Host == common.APIHTTPAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestHost(r *http.Request) string {
|
||||||
|
// check if it's from backend
|
||||||
|
if IsFrontend(r) {
|
||||||
|
return r.Header.Get("X-Forwarded-Host")
|
||||||
|
}
|
||||||
|
return r.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
// cookieDomain returns the fully qualified domain name of the request host
|
||||||
|
// with subdomain stripped.
|
||||||
|
//
|
||||||
|
// If the request host does not have a subdomain,
|
||||||
|
// an empty string is returned
|
||||||
|
//
|
||||||
|
// "abc.example.com" -> ".example.com" (cross subdomain)
|
||||||
|
// "example.com" -> "" (same domain only)
|
||||||
|
func cookieDomain(r *http.Request) string {
|
||||||
|
parts := strutils.SplitRune(requestHost(r), '.')
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
parts[0] = ""
|
||||||
|
return strutils.JoinRune(parts, '.')
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
MaxAge: int(ttl.Seconds()),
|
||||||
|
Domain: cookieDomain(r),
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: common.APIJWTSecure,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearTokenCookie(w http.ResponseWriter, r *http.Request, name string) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: name,
|
||||||
|
Value: "",
|
||||||
|
MaxAge: -1,
|
||||||
|
Domain: cookieDomain(r),
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: common.APIJWTSecure,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
}
|
|
@ -13,21 +13,17 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"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"`
|
Options map[string]any `json:"options,omitempty"`
|
||||||
}
|
}
|
||||||
ProviderOpt map[string]any
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrMissingDomain = gperr.New("missing field 'domains'")
|
ErrMissingDomain = gperr.New("missing field 'domains'")
|
||||||
|
@ -37,10 +33,15 @@ var (
|
||||||
ErrUnknownProvider = gperr.New("unknown provider")
|
ErrUnknownProvider = gperr.New("unknown provider")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProviderLocal = "local"
|
||||||
|
ProviderPseudo = "pseudo"
|
||||||
|
)
|
||||||
|
|
||||||
var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
|
var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
|
||||||
|
|
||||||
// Validate implements the utils.CustomValidator interface.
|
// Validate implements the utils.CustomValidator interface.
|
||||||
func (cfg *AutocertConfig) Validate() gperr.Error {
|
func (cfg *Config) Validate() gperr.Error {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -64,11 +65,11 @@ func (cfg *AutocertConfig) Validate() gperr.Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// check if provider is implemented
|
// check if provider is implemented
|
||||||
providerConstructor, ok := providersGenMap[cfg.Provider]
|
providerConstructor, ok := Providers[cfg.Provider]
|
||||||
if !ok {
|
if !ok {
|
||||||
b.Add(ErrUnknownProvider.
|
b.Add(ErrUnknownProvider.
|
||||||
Subject(cfg.Provider).
|
Subject(cfg.Provider).
|
||||||
Withf(strutils.DoYouMean(utils.NearestField(cfg.Provider, providersGenMap))))
|
With(gperr.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
|
||||||
} else {
|
} else {
|
||||||
_, err := providerConstructor(cfg.Options)
|
_, err := providerConstructor(cfg.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -79,13 +80,9 @@ func (cfg *AutocertConfig) Validate() gperr.Error {
|
||||||
return b.Error()
|
return b.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AutocertConfig) GetProvider() (*Provider, gperr.Error) {
|
func (cfg *Config) GetLegoConfig() (*User, *lego.Config, gperr.Error) {
|
||||||
if cfg == nil {
|
|
||||||
cfg = new(AutocertConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfg.Validate(); err != nil {
|
if err := cfg.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.CertPath == "" {
|
if cfg.CertPath == "" {
|
||||||
|
@ -102,35 +99,31 @@ func (cfg *AutocertConfig) GetProvider() (*Provider, gperr.Error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if cfg.Provider != ProviderLocal && cfg.Provider != ProviderPseudo {
|
if cfg.Provider != ProviderLocal && cfg.Provider != ProviderPseudo {
|
||||||
if privKey, err = cfg.loadACMEKey(); err != nil {
|
if privKey, err = cfg.LoadACMEKey(); err != nil {
|
||||||
logging.Info().Err(err).Msg("load ACME private key failed")
|
logging.Info().Err(err).Msg("load ACME private key failed")
|
||||||
logging.Info().Msg("generate new ACME private key")
|
logging.Info().Msg("generate new ACME private key")
|
||||||
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gperr.New("generate ACME private key").With(err)
|
return nil, nil, gperr.New("generate ACME private key").With(err)
|
||||||
}
|
}
|
||||||
if err = cfg.saveACMEKey(privKey); err != nil {
|
if err = cfg.SaveACMEKey(privKey); err != nil {
|
||||||
return nil, gperr.New("save ACME private key").With(err)
|
return nil, nil, gperr.New("save ACME private key").With(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &User{
|
user := &User{
|
||||||
Email: cfg.Email,
|
Email: cfg.Email,
|
||||||
key: privKey,
|
Key: privKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
legoCfg := lego.NewConfig(user)
|
legoCfg := lego.NewConfig(user)
|
||||||
legoCfg.Certificate.KeyType = certcrypto.RSA2048
|
legoCfg.Certificate.KeyType = certcrypto.RSA2048
|
||||||
|
|
||||||
return &Provider{
|
return user, legoCfg, nil
|
||||||
cfg: cfg,
|
|
||||||
user: user,
|
|
||||||
legoCfg: legoCfg,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AutocertConfig) loadACMEKey() (*ecdsa.PrivateKey, error) {
|
func (cfg *Config) LoadACMEKey() (*ecdsa.PrivateKey, error) {
|
||||||
data, err := os.ReadFile(cfg.ACMEKeyPath)
|
data, err := os.ReadFile(cfg.ACMEKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -138,7 +131,7 @@ func (cfg *AutocertConfig) loadACMEKey() (*ecdsa.PrivateKey, error) {
|
||||||
return x509.ParseECPrivateKey(data)
|
return x509.ParseECPrivateKey(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AutocertConfig) saveACMEKey(key *ecdsa.PrivateKey) error {
|
func (cfg *Config) SaveACMEKey(key *ecdsa.PrivateKey) error {
|
||||||
data, err := x509.MarshalECPrivateKey(key)
|
data, err := x509.MarshalECPrivateKey(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
package autocert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/porkbun"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
certBasePath = "certs/"
|
|
||||||
CertFileDefault = certBasePath + "cert.crt"
|
|
||||||
KeyFileDefault = certBasePath + "priv.key"
|
|
||||||
ACMEKeyFileDefault = certBasePath + "acme.key"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProviderLocal = "local"
|
|
||||||
ProviderCloudflare = "cloudflare"
|
|
||||||
ProviderClouddns = "clouddns"
|
|
||||||
ProviderDuckdns = "duckdns"
|
|
||||||
ProviderOVH = "ovh"
|
|
||||||
ProviderPseudo = "pseudo" // for testing
|
|
||||||
ProviderPorkbun = "porkbun"
|
|
||||||
)
|
|
||||||
|
|
||||||
var providersGenMap = map[string]ProviderGenerator{
|
|
||||||
ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
|
||||||
ProviderCloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
|
||||||
ProviderClouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
|
||||||
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
|
||||||
ProviderOVH: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
|
|
||||||
ProviderPseudo: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
|
||||||
ProviderPorkbun: providerGenerator(porkbun.NewDefaultConfig, porkbun.NewDNSProviderConfig),
|
|
||||||
}
|
|
8
internal/autocert/paths.go
Normal file
8
internal/autocert/paths.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package autocert
|
||||||
|
|
||||||
|
const (
|
||||||
|
certBasePath = "certs/"
|
||||||
|
CertFileDefault = certBasePath + "cert.crt"
|
||||||
|
KeyFileDefault = certBasePath + "priv.key"
|
||||||
|
ACMEKeyFileDefault = certBasePath + "acme.key"
|
||||||
|
)
|
|
@ -9,23 +9,22 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"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/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/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
|
||||||
|
@ -33,16 +32,21 @@ type (
|
||||||
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")
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -188,8 +192,18 @@ func (p *Provider) ScheduleRenewal(parent task.Parent) {
|
||||||
if err := p.renewIfNeeded(); err != nil {
|
if err := p.renewIfNeeded(); err != nil {
|
||||||
gperr.LogWarn("cert renew failed", err)
|
gperr.LogWarn("cert renew failed", err)
|
||||||
lastErrOn = time.Now()
|
lastErrOn = time.Now()
|
||||||
|
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{}
|
lastErrOn = time.Time{}
|
||||||
renewalTime = p.ShouldRenewOn()
|
renewalTime = p.ShouldRenewOn()
|
||||||
|
@ -205,7 +219,7 @@ func (p *Provider) initClient() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
generator := providersGenMap[p.cfg.Provider]
|
generator := Providers[p.cfg.Provider]
|
||||||
legoProvider, pErr := generator(p.cfg.Options)
|
legoProvider, pErr := generator(p.cfg.Options)
|
||||||
if pErr != nil {
|
if pErr != nil {
|
||||||
return pErr
|
return pErr
|
||||||
|
@ -322,18 +336,3 @@ func getCertExpiries(cert *tls.Certificate) (CertExpiries, error) {
|
||||||
}
|
}
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerGenerator[CT any, PT challenge.Provider](
|
|
||||||
defaultCfg func() *CT,
|
|
||||||
newProvider func(*CT) (PT, error),
|
|
||||||
) ProviderGenerator {
|
|
||||||
return func(opt ProviderOpt) (challenge.Provider, gperr.Error) {
|
|
||||||
cfg := defaultCfg()
|
|
||||||
err := U.Deserialize(opt, &cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p, pErr := newProvider(cfg)
|
|
||||||
return p, gperr.Wrap(pErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
"github.com/goccy/go-yaml"
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/yaml.v3"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// type Config struct {
|
// type Config struct {
|
||||||
|
@ -44,7 +44,7 @@ oauth2_config:
|
||||||
}
|
}
|
||||||
testYaml = testYaml[1:] // remove first \n
|
testYaml = testYaml[1:] // remove first \n
|
||||||
opt := make(map[string]any)
|
opt := make(map[string]any)
|
||||||
ExpectNoError(t, yaml.Unmarshal([]byte(testYaml), opt))
|
require.NoError(t, yaml.Unmarshal([]byte(testYaml), &opt))
|
||||||
ExpectNoError(t, U.Deserialize(opt, cfg))
|
require.NoError(t, utils.MapUnmarshalValidate(opt, cfg))
|
||||||
ExpectEqual(t, cfg, cfgExpected)
|
require.Equal(t, cfgExpected, cfg)
|
||||||
}
|
}
|
||||||
|
|
26
internal/autocert/providers.go
Normal file
26
internal/autocert/providers.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package autocert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-acme/lego/v4/challenge"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Generator func(map[string]any) (challenge.Provider, gperr.Error)
|
||||||
|
|
||||||
|
var Providers = make(map[string]Generator)
|
||||||
|
|
||||||
|
func DNSProvider[CT any, PT challenge.Provider](
|
||||||
|
defaultCfg func() *CT,
|
||||||
|
newProvider func(*CT) (PT, error),
|
||||||
|
) Generator {
|
||||||
|
return func(opt map[string]any) (challenge.Provider, gperr.Error) {
|
||||||
|
cfg := defaultCfg()
|
||||||
|
err := utils.MapUnmarshalValidate(opt, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p, pErr := newProvider(cfg)
|
||||||
|
return p, gperr.Wrap(pErr)
|
||||||
|
}
|
||||||
|
}
|
14
internal/autocert/types/provider.go
Normal file
14
internal/autocert/types/provider.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package autocert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
Setup() error
|
||||||
|
GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||||
|
ScheduleRenewal(task.Parent)
|
||||||
|
ObtainCert() error
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import (
|
||||||
type User struct {
|
type User struct {
|
||||||
Email string
|
Email string
|
||||||
Registration *registration.Resource
|
Registration *registration.Resource
|
||||||
key crypto.PrivateKey
|
Key crypto.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) GetEmail() string {
|
func (u *User) GetEmail() string {
|
||||||
|
@ -21,5 +21,5 @@ func (u *User) GetRegistration() *registration.Resource {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) GetPrivateKey() crypto.PrivateKey {
|
func (u *User) GetPrivateKey() crypto.PrivateKey {
|
||||||
return u.key
|
return u.Key
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,22 +14,23 @@ const (
|
||||||
ConfigFileName = "config.yml"
|
ConfigFileName = "config.yml"
|
||||||
ConfigExampleFileName = "config.example.yml"
|
ConfigExampleFileName = "config.example.yml"
|
||||||
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
||||||
HomepageJSONConfigPath = ConfigBasePath + "/.homepage.json"
|
|
||||||
IconListCachePath = ConfigBasePath + "/.icon_list_cache.json"
|
DataDir = "data"
|
||||||
IconCachePath = ConfigBasePath + "/.icon_cache.json"
|
IconListCachePath = DataDir + "/.icon_list_cache.json"
|
||||||
|
|
||||||
|
NamespaceHomepageOverrides = ".homepage"
|
||||||
|
NamespaceIconCache = ".icon_cache"
|
||||||
|
|
||||||
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
|
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
|
||||||
|
|
||||||
ComposeFileName = "compose.yml"
|
ComposeFileName = "compose.yml"
|
||||||
ComposeExampleFileName = "compose.example.yml"
|
ComposeExampleFileName = "compose.example.yml"
|
||||||
|
|
||||||
ErrorPagesBasePath = "error_pages"
|
ErrorPagesBasePath = "error_pages"
|
||||||
|
|
||||||
AgentCertsBasePath = "certs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var RequiredDirectories = []string{
|
var RequiredDirectories = []string{
|
||||||
ConfigBasePath,
|
ConfigBasePath,
|
||||||
|
DataDir,
|
||||||
ErrorPagesBasePath,
|
ErrorPagesBasePath,
|
||||||
MiddlewareComposeBasePath,
|
MiddlewareComposeBasePath,
|
||||||
}
|
}
|
||||||
|
@ -40,7 +41,7 @@ const (
|
||||||
HealthCheckIntervalDefault = 5 * time.Second
|
HealthCheckIntervalDefault = 5 * time.Second
|
||||||
HealthCheckTimeoutDefault = 5 * time.Second
|
HealthCheckTimeoutDefault = 5 * time.Second
|
||||||
|
|
||||||
WakeTimeoutDefault = "30s"
|
WakeTimeoutDefault = "3m"
|
||||||
StopTimeoutDefault = "30s"
|
StopTimeoutDefault = "3m"
|
||||||
StopMethodDefault = "stop"
|
StopMethodDefault = "stop"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.Panic().Err(err).Msg("failed to decode jwt key")
|
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.Panic().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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ var (
|
||||||
IsDebug = GetEnvBool("DEBUG", IsTest)
|
IsDebug = GetEnvBool("DEBUG", IsTest)
|
||||||
IsTrace = GetEnvBool("TRACE", false) && IsDebug
|
IsTrace = GetEnvBool("TRACE", false) && IsDebug
|
||||||
|
|
||||||
|
HTTP3Enabled = GetEnvBool("HTTP3_ENABLED", true)
|
||||||
|
|
||||||
ProxyHTTPAddr,
|
ProxyHTTPAddr,
|
||||||
ProxyHTTPHost,
|
ProxyHTTPHost,
|
||||||
ProxyHTTPPort,
|
ProxyHTTPPort,
|
||||||
|
@ -34,11 +36,9 @@ var (
|
||||||
APIHTTPPort,
|
APIHTTPPort,
|
||||||
APIHTTPURL = GetAddrEnv("API_ADDR", "127.0.0.1:8888", "http")
|
APIHTTPURL = GetAddrEnv("API_ADDR", "127.0.0.1:8888", "http")
|
||||||
|
|
||||||
PrometheusEnabled = GetEnvBool("PROMETHEUS_ENABLED", false)
|
|
||||||
|
|
||||||
APIJWTSecure = GetEnvBool("API_JWT_SECURE", true)
|
APIJWTSecure = GetEnvBool("API_JWT_SECURE", true)
|
||||||
APIJWTSecret = decodeJWTKey(GetEnvString("API_JWT_SECRET", ""))
|
APIJWTSecret = decodeJWTKey(GetEnvString("API_JWT_SECRET", ""))
|
||||||
APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", time.Hour)
|
APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", 24*time.Hour)
|
||||||
APIUser = GetEnvString("API_USER", "admin")
|
APIUser = GetEnvString("API_USER", "admin")
|
||||||
APIPassword = GetEnvString("API_PASSWORD", "password")
|
APIPassword = GetEnvString("API_PASSWORD", "password")
|
||||||
|
|
||||||
|
@ -48,8 +48,7 @@ var (
|
||||||
OIDCIssuerURL = GetEnvString("OIDC_ISSUER_URL", "")
|
OIDCIssuerURL = GetEnvString("OIDC_ISSUER_URL", "")
|
||||||
OIDCClientID = GetEnvString("OIDC_CLIENT_ID", "")
|
OIDCClientID = GetEnvString("OIDC_CLIENT_ID", "")
|
||||||
OIDCClientSecret = GetEnvString("OIDC_CLIENT_SECRET", "")
|
OIDCClientSecret = GetEnvString("OIDC_CLIENT_SECRET", "")
|
||||||
OIDCRedirectURL = GetEnvString("OIDC_REDIRECT_URL", "")
|
OIDCScopes = GetCommaSepEnv("OIDC_SCOPES", "openid, profile, email, groups")
|
||||||
OIDCScopes = GetEnvString("OIDC_SCOPES", "openid, profile, email")
|
|
||||||
OIDCAllowedUsers = GetCommaSepEnv("OIDC_ALLOWED_USERS", "")
|
OIDCAllowedUsers = GetCommaSepEnv("OIDC_ALLOWED_USERS", "")
|
||||||
OIDCAllowedGroups = GetCommaSepEnv("OIDC_ALLOWED_GROUPS", "")
|
OIDCAllowedGroups = GetCommaSepEnv("OIDC_ALLOWED_GROUPS", "")
|
||||||
|
|
||||||
|
@ -59,6 +58,8 @@ var (
|
||||||
MetricsDisableDisk = GetEnvBool("METRICS_DISABLE_DISK", false)
|
MetricsDisableDisk = GetEnvBool("METRICS_DISABLE_DISK", false)
|
||||||
MetricsDisableNetwork = GetEnvBool("METRICS_DISABLE_NETWORK", false)
|
MetricsDisableNetwork = GetEnvBool("METRICS_DISABLE_NETWORK", false)
|
||||||
MetricsDisableSensors = GetEnvBool("METRICS_DISABLE_SENSORS", false)
|
MetricsDisableSensors = GetEnvBool("METRICS_DISABLE_SENSORS", false)
|
||||||
|
|
||||||
|
ForceResolveCountry = GetEnvBool("FORCE_RESOLVE_COUNTRY", false)
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T {
|
func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T {
|
||||||
|
@ -77,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 {
|
||||||
|
@ -102,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"
|
||||||
|
@ -110,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
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PE
|
||||||
|
|
||||||
var agentCfg agent.AgentConfig
|
var agentCfg agent.AgentConfig
|
||||||
agentCfg.Addr = host
|
agentCfg.Addr = host
|
||||||
err := agentCfg.StartWithCerts(cfg.Task(), ca.Cert, client.Cert, client.Key)
|
err := agentCfg.StartWithCerts(cfg.Task().Context(), ca.Cert, client.Cert, client.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, gperr.Wrap(err, "failed to start agent")
|
return 0, gperr.Wrap(err, "failed to start agent")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,19 +9,23 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"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/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/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"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"
|
||||||
)
|
)
|
||||||
|
@ -114,7 +118,7 @@ func Reload() gperr.Error {
|
||||||
err := newCfg.load()
|
err := newCfg.load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
newCfg.task.Finish(err)
|
newCfg.task.Finish(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
|
||||||
|
@ -197,6 +201,7 @@ func (cfg *Config) StartServers(opts ...*StartServersOptions) {
|
||||||
HTTPAddr: common.ProxyHTTPAddr,
|
HTTPAddr: common.ProxyHTTPAddr,
|
||||||
HTTPSAddr: common.ProxyHTTPSAddr,
|
HTTPSAddr: common.ProxyHTTPSAddr,
|
||||||
Handler: cfg.entrypoint,
|
Handler: cfg.entrypoint,
|
||||||
|
ACL: cfg.value.ACL,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if opt.API {
|
if opt.API {
|
||||||
|
@ -218,7 +223,7 @@ func (cfg *Config) load() gperr.Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
model := config.DefaultConfig()
|
model := config.DefaultConfig()
|
||||||
if err := utils.DeserializeYAML(data, model); err != nil {
|
if err := utils.UnmarshalValidateYAML(data, model); err != nil {
|
||||||
gperr.LogFatal(errMsg, err)
|
gperr.LogFatal(errMsg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,8 +231,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
|
||||||
|
@ -237,9 +244,30 @@ func (cfg *Config) load() gperr.Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cfg.entrypoint.SetFindRouteDomains(model.MatchDomains)
|
cfg.entrypoint.SetFindRouteDomains(model.MatchDomains)
|
||||||
|
if model.ACL.Valid() {
|
||||||
|
err := model.ACL.Start(cfg.task)
|
||||||
|
if err != nil {
|
||||||
|
errs.Add(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
@ -251,13 +279,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 {
|
||||||
|
@ -278,8 +326,8 @@ func (cfg *Config) loadRouteProviders(providers *config.Providers) gperr.Error {
|
||||||
removeAllAgents()
|
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)
|
addAgent(agent)
|
||||||
|
@ -319,6 +367,7 @@ func (cfg *Config) loadRouteProviders(providers *config.Providers) gperr.Error {
|
||||||
lenLongestName = len(k)
|
lenLongestName = len(k)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
results.EnableConcurrency()
|
||||||
cfg.providers.RangeAllParallel(func(_ string, p *proxy.Provider) {
|
cfg.providers.RangeAllParallel(func(_ string, p *proxy.Provider) {
|
||||||
if err := p.LoadRoutes(); err != nil {
|
if err := p.LoadRoutes(); err != nil {
|
||||||
errs.Add(err.Subject(p.String()))
|
errs.Add(err.Subject(p.String()))
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
"github.com/yusing/go-proxy/internal/route"
|
"github.com/yusing/go-proxy/internal/route"
|
||||||
"github.com/yusing/go-proxy/internal/route/provider"
|
"github.com/yusing/go-proxy/internal/route/provider"
|
||||||
)
|
)
|
||||||
|
@ -23,10 +24,13 @@ func (cfg *Config) DumpRouteProviders() map[string]*provider.Provider {
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) RouteProviderList() []string {
|
func (cfg *Config) RouteProviderList() []config.RouteProviderListResponse {
|
||||||
var list []string
|
var list []config.RouteProviderListResponse
|
||||||
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
||||||
list = append(list, p.ShortName())
|
list = append(list, config.RouteProviderListResponse{
|
||||||
|
ShortName: p.ShortName(),
|
||||||
|
FullName: p.String(),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package types
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -7,16 +7,20 @@ import (
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/go-proxy/internal/acl"
|
||||||
"github.com/yusing/go-proxy/internal/autocert"
|
"github.com/yusing/go-proxy/internal/autocert"
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/accesslog"
|
"github.com/yusing/go-proxy/internal/logging/accesslog"
|
||||||
|
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/proxmox"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Config struct {
|
Config struct {
|
||||||
AutoCert *autocert.AutocertConfig `json:"autocert"`
|
ACL *acl.Config `json:"acl"`
|
||||||
|
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"`
|
||||||
|
@ -28,17 +32,25 @@ 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"`
|
||||||
AccessLog *accesslog.Config `json:"access_log" validate:"omitempty"`
|
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
HomepageConfig struct {
|
||||||
|
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)
|
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)
|
||||||
|
@ -81,7 +93,7 @@ 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 utils.UnmarshalValidateYAML(data, &model)
|
||||||
}
|
}
|
||||||
|
|
||||||
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)
|
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
package types
|
|
||||||
|
|
||||||
type HomepageConfig struct {
|
|
||||||
UseDefaultCategories bool `json:"use_default_categories"`
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package autocert
|
package dnsproviders
|
||||||
|
|
||||||
type DummyConfig struct{}
|
type DummyConfig struct{}
|
||||||
type DummyProvider struct{}
|
type DummyProvider struct{}
|
55
internal/dnsproviders/gen.py
Normal file
55
internal/dnsproviders/gen.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
class Entry:
|
||||||
|
def __init__(self, name: str, type: str, **kwargs) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.type = type
|
||||||
|
|
||||||
|
url = "https://api.github.com/repos/go-acme/lego/contents/providers/dns"
|
||||||
|
response = requests.get(url)
|
||||||
|
data: list[Entry] = [Entry(**i) for i in response.json()]
|
||||||
|
|
||||||
|
header = "//go:generate /usr/bin/python3 gen.py\n\npackage dnsproviders\n\n"
|
||||||
|
names: list[str] = [
|
||||||
|
"Local = \"local\"",
|
||||||
|
"Pseudo = \"pseudo\"",
|
||||||
|
]
|
||||||
|
imports: list[str] = [
|
||||||
|
"\"github.com/yusing/go-proxy/internal/autocert\""
|
||||||
|
]
|
||||||
|
genMap: list[str] = [
|
||||||
|
"autocert.Providers[Local] = autocert.DNSProvider(NewDummyDefaultConfig, NewDummyDNSProviderConfig)",
|
||||||
|
"autocert.Providers[Pseudo] = autocert.DNSProvider(NewDummyDefaultConfig, NewDummyDNSProviderConfig)",
|
||||||
|
]
|
||||||
|
|
||||||
|
blacklists = [
|
||||||
|
"internal",
|
||||||
|
# deprecated
|
||||||
|
"azure",
|
||||||
|
"brandit",
|
||||||
|
"cloudxns",
|
||||||
|
"dnspod",
|
||||||
|
"mythicbeasts",
|
||||||
|
"yandexcloud"
|
||||||
|
]
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
if item.type != "dir" or item.name in blacklists:
|
||||||
|
continue
|
||||||
|
imports.append(f"\"github.com/go-acme/lego/v4/providers/dns/{item.name}\"")
|
||||||
|
genMap.append(f"autocert.Providers[\"{item.name}\"] = autocert.DNSProvider({item.name}.NewDefaultConfig, {item.name}.NewDNSProviderConfig)")
|
||||||
|
|
||||||
|
with open("providers.go", "w") as f:
|
||||||
|
f.write(header)
|
||||||
|
f.write("import (\n")
|
||||||
|
f.write("\n".join(imports))
|
||||||
|
f.write("\n)\n\n")
|
||||||
|
f.write("const (\n")
|
||||||
|
f.write("\n".join(names))
|
||||||
|
f.write("\n)\n\n")
|
||||||
|
f.write("func InitProviders() {\n")
|
||||||
|
f.write("\n".join(genMap))
|
||||||
|
f.write("\n}\n\n")
|
||||||
|
|
||||||
|
os.execvp("go", ["go", "fmt", "providers.go"])
|
194
internal/dnsproviders/go.mod
Normal file
194
internal/dnsproviders/go.mod
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
module github.com/yusing/go-proxy/internal/dnsproviders
|
||||||
|
|
||||||
|
go 1.24.3
|
||||||
|
|
||||||
|
replace github.com/yusing/go-proxy => ../..
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-acme/lego/v4 v4.23.1
|
||||||
|
github.com/yusing/go-proxy v0.0.0-00010101000000-000000000000
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/auth v0.16.1 // indirect
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||||
|
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||||
|
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||||
|
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
||||||
|
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.2 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.51.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||||
|
github.com/aws/smithy-go v1.22.3 // indirect
|
||||||
|
github.com/baidubce/bce-sdk-go v0.9.226 // indirect
|
||||||
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
|
github.com/boombuler/barcode v1.0.2 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/civo/civogo v0.5.0 // indirect
|
||||||
|
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||||
|
github.com/exoscale/egoscale/v3 v3.1.17 // indirect
|
||||||
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
|
github.com/go-errors/errors v1.5.1 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
|
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.17.1 // indirect
|
||||||
|
github.com/gofrs/flock v0.12.1 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.14.2 // 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.7 // indirect
|
||||||
|
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||||
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.149 // indirect
|
||||||
|
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||||
|
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
||||||
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||||
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
|
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||||
|
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/linode/linodego v1.50.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.66 // indirect
|
||||||
|
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||||
|
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||||
|
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
|
||||||
|
github.com/nrdcg/desec v0.11.0 // indirect
|
||||||
|
github.com/nrdcg/freemyip v0.3.0 // indirect
|
||||||
|
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||||
|
github.com/nrdcg/goinwx v0.11.0 // indirect
|
||||||
|
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||||
|
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||||
|
github.com/nrdcg/nodion v0.1.0 // indirect
|
||||||
|
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||||
|
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
|
github.com/oracle/oci-go-sdk/v65 v65.91.0 // indirect
|
||||||
|
github.com/ovh/go-ovh v1.7.0 // indirect
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/peterhellberg/link v1.2.0 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/pquerna/otp v1.4.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.2.10 // indirect
|
||||||
|
github.com/sacloud/go-http v0.1.9 // indirect
|
||||||
|
github.com/sacloud/iaas-api-go v1.15.0 // indirect
|
||||||
|
github.com/sacloud/packages-go v0.0.11 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 // indirect
|
||||||
|
github.com/selectel/domains-go v1.1.0 // indirect
|
||||||
|
github.com/selectel/go-selvpcclient/v3 v3.2.1 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||||
|
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||||
|
github.com/softlayer/softlayer-go v1.1.7 // indirect
|
||||||
|
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||||
|
github.com/sony/gobreaker v1.0.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
|
github.com/spf13/cast v1.8.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
|
github.com/spf13/viper v1.20.1 // indirect
|
||||||
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1164 // indirect
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 // indirect
|
||||||
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
|
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||||
|
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
||||||
|
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||||
|
github.com/volcengine/volc-sdk-golang v1.0.207 // indirect
|
||||||
|
github.com/vultr/govultr/v3 v3.20.0 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/ratelimit v0.3.1 // indirect
|
||||||
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
|
golang.org/x/net v0.40.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
|
golang.org/x/sync v0.14.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/text v0.25.0 // indirect
|
||||||
|
golang.org/x/time v0.11.0 // indirect
|
||||||
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
|
google.golang.org/api v0.233.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
|
||||||
|
google.golang.org/grpc v1.72.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/ns1/ns1-go.v2 v2.14.3 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
k8s.io/api v0.33.0 // indirect
|
||||||
|
k8s.io/apimachinery v0.33.0 // indirect
|
||||||
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
|
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect
|
||||||
|
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||||
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
|
)
|
2474
internal/dnsproviders/go.sum
Normal file
2474
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