mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-17 05:59:42 +02:00
Compare commits
88 Commits
v0.18.2
...
feat/custo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7730585bc2 | ||
|
|
e2717c9e44 | ||
|
|
af8bf197c9 | ||
|
|
25d5fee05f | ||
|
|
d7f8359f27 | ||
|
|
81a6ef9745 | ||
|
|
973a44ee07 | ||
|
|
80bc018a7f | ||
|
|
24118fa57c | ||
|
|
57292f0fe8 | ||
|
|
54e4969d26 | ||
|
|
feb9947543 | ||
|
|
c2b606e63e | ||
|
|
cdfc9d553b | ||
|
|
75fd8d1fdc | ||
|
|
57f80344bc | ||
|
|
c286275f7e | ||
|
|
88f3a95b61 | ||
|
|
104e1b1d0a | ||
|
|
82f48d1248 | ||
|
|
47fb07fe4d | ||
|
|
4615d7dd4e | ||
|
|
18ab6c52ec | ||
|
|
3b4deccd8e | ||
|
|
7e56fce4c9 | ||
|
|
49d062a94b | ||
|
|
4e7684d67d | ||
|
|
76b0505b88 | ||
|
|
4d88d59100 | ||
|
|
08b262d94b | ||
|
|
69bc3acf15 | ||
|
|
82e2705f44 | ||
|
|
dc1102905b | ||
|
|
eb7495b02a | ||
|
|
2ec1de96d5 | ||
|
|
57da345335 | ||
|
|
53a78706e4 | ||
|
|
dcd21b2374 | ||
|
|
a2e253591c | ||
|
|
fa16f4150a | ||
|
|
d8eff90acc | ||
|
|
5cdbe81beb | ||
|
|
ffea5fb3da | ||
|
|
3f2dfe14b5 | ||
|
|
be87d47ebb | ||
|
|
8c6fe38edb | ||
|
|
a478dab97b | ||
|
|
fce96ff3be | ||
|
|
1eac48e899 | ||
|
|
fdbf1ad787 | ||
|
|
90214ff752 | ||
|
|
12a63a66f6 | ||
|
|
2b44ac5bcb | ||
|
|
0d859cc36f | ||
|
|
65c063a838 | ||
|
|
a2c9e47557 | ||
|
|
1380b58141 | ||
|
|
d1524c1013 | ||
|
|
3cd9e47fd0 | ||
|
|
e3699b406c | ||
|
|
6a5d324733 | ||
|
|
fb075a24d7 | ||
|
|
5d2b700cb2 | ||
|
|
49ee9c908a | ||
|
|
658332005d | ||
|
|
1e8cb04b7c | ||
|
|
1c892a35f7 | ||
|
|
de2383eed7 | ||
|
|
c59567ae8f | ||
|
|
8ed63fe4b0 | ||
|
|
111d767d46 | ||
|
|
5da9dd6082 | ||
|
|
edb4b59254 | ||
|
|
e823172c31 | ||
|
|
4d030d2e16 | ||
|
|
fb217cf80e | ||
|
|
3689e72eff | ||
|
|
26bea0d21d | ||
|
|
9a5553a5b8 | ||
|
|
df24acb4af | ||
|
|
b53dd17b84 | ||
|
|
73a5c57d67 | ||
|
|
253e06923d | ||
|
|
2c0d58f692 | ||
|
|
864a43266d | ||
|
|
477ddb6241 | ||
|
|
fdac2853af | ||
|
|
8e37627371 |
49
.env.example
49
.env.example
@@ -1,35 +1,27 @@
|
|||||||
# docker image tag (latest, nightly)
|
|
||||||
TAG=latest
|
|
||||||
|
|
||||||
# set timezone to get correct log timestamp
|
# set timezone to get correct log timestamp
|
||||||
TZ=ETC/UTC
|
TZ=ETC/UTC
|
||||||
|
|
||||||
# container uid and gid (must match the owner of mounted directories)
|
|
||||||
GODOXY_UID=1000
|
|
||||||
GODOXY_GID=1000
|
|
||||||
|
|
||||||
# Set GODOXY_API_JWT_SECURE=false to allow http
|
|
||||||
GODOXY_API_JWT_SECURE=true
|
|
||||||
# API JWT Configuration (common)
|
|
||||||
# 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
|
||||||
|
|
||||||
|
# Enable `secure` cookie flag
|
||||||
|
GODOXY_API_JWT_SECURE=true
|
||||||
|
# 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
|
||||||
# GODOXY_OIDC_SCOPES=openid, profile, email, groups # you may also include `offline_access` if your Idp supports it (e.g. Authentik, Pocket ID)
|
# Keep /api/auth/callback as the redirect URL, change the domain to match your setup.
|
||||||
|
# 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:
|
||||||
@@ -50,29 +42,14 @@ GODOXY_API_PASSWORD=password
|
|||||||
GODOXY_HTTP_ADDR=:80
|
GODOXY_HTTP_ADDR=:80
|
||||||
GODOXY_HTTPS_ADDR=:443
|
GODOXY_HTTPS_ADDR=:443
|
||||||
|
|
||||||
# Enable HTTP3
|
|
||||||
GODOXY_HTTP3_ENABLED=true
|
|
||||||
|
|
||||||
# API listening address
|
# API listening address
|
||||||
GODOXY_API_ADDR=127.0.0.1:8888
|
GODOXY_API_ADDR=127.0.0.1:8888
|
||||||
|
|
||||||
# Metrics
|
|
||||||
GODOXY_METRICS_DISABLE_CPU=false
|
|
||||||
GODOXY_METRICS_DISABLE_MEMORY=false
|
|
||||||
GODOXY_METRICS_DISABLE_DISK=false
|
|
||||||
GODOXY_METRICS_DISABLE_NETWORK=false
|
|
||||||
GODOXY_METRICS_DISABLE_SENSORS=false
|
|
||||||
|
|
||||||
# Frontend listening port
|
# Frontend listening port
|
||||||
GODOXY_FRONTEND_PORT=3000
|
GODOXY_FRONTEND_PORT=3000
|
||||||
|
|
||||||
# Frontend aliases (subdomains / FQDNs, e.g. godoxy, godoxy.domain.com)
|
# Prometheus Metrics
|
||||||
GODOXY_FRONTEND_ALIASES=godoxy
|
GODOXY_PROMETHEUS_ENABLED=true
|
||||||
|
|
||||||
# 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,6 +36,9 @@ 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,10 +15,9 @@ 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
|
||||||
target: agent
|
agent: true
|
||||||
|
|||||||
3
.github/workflows/docker-image-prod.yml
vendored
3
.github/workflows/docker-image-prod.yml
vendored
@@ -12,10 +12,9 @@ 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
|
||||||
target: agent
|
agent: true
|
||||||
|
|||||||
23
.github/workflows/docker-image-socket-proxy.yml
vendored
23
.github/workflows/docker-image-socket-proxy.yml
vendored
@@ -1,23 +0,0 @@
|
|||||||
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,20 +12,16 @@ on:
|
|||||||
old_image_name:
|
old_image_name:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
target:
|
agent:
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
dockerfile:
|
|
||||||
required: false
|
required: false
|
||||||
type: string
|
default: false
|
||||||
default: Dockerfile
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
MAKE_ARGS: ${{ inputs.target }}=1
|
MAKE_ARGS: agent=${{ inputs.agent && '1' || '0' }}
|
||||||
DIGEST_PATH: /tmp/digests/${{ inputs.target }}
|
DIGEST_PATH: /tmp/digests/${{ inputs.agent && 'agent' || 'main' }}
|
||||||
DIGEST_NAME_SUFFIX: ${{ inputs.target }}
|
DIGEST_NAME_SUFFIX: ${{ inputs.agent && 'agent' || 'main' }}
|
||||||
DOCKERFILE: ${{ inputs.dockerfile }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -80,14 +76,11 @@ 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 }}
|
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}-${{ inputs.tag }}
|
||||||
# type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }}
|
|
||||||
cache-to: |
|
cache-to: |
|
||||||
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max
|
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}-${{ inputs.tag }},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 }}
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -29,14 +29,10 @@ todo.md
|
|||||||
.aider*
|
.aider*
|
||||||
mtrace.json
|
mtrace.json
|
||||||
.env
|
.env
|
||||||
*.env
|
|
||||||
.cursorrules
|
|
||||||
.cursor/
|
|
||||||
.windsurfrules
|
|
||||||
test.Dockerfile
|
test.Dockerfile
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
|
|
||||||
!agent.compose.yml
|
!agent.compose.yml
|
||||||
!agent/pkg/**
|
!agent/pkg/**
|
||||||
282
.golangci.yml
282
.golangci.yml
@@ -1,153 +1,135 @@
|
|||||||
version: "2"
|
run:
|
||||||
|
timeout: 10m
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
govet:
|
||||||
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
- shadow
|
||||||
|
- fieldalignment
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 14
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
funlen:
|
||||||
|
lines: -1
|
||||||
|
statements: 120
|
||||||
|
forbidigo:
|
||||||
|
forbid:
|
||||||
|
- ^print(ln)?$
|
||||||
|
godox:
|
||||||
|
keywords:
|
||||||
|
- FIXME
|
||||||
|
tagalign:
|
||||||
|
align: false
|
||||||
|
sort: true
|
||||||
|
order:
|
||||||
|
- description
|
||||||
|
- json
|
||||||
|
- toml
|
||||||
|
- yaml
|
||||||
|
- yml
|
||||||
|
- label
|
||||||
|
- label-slice-as-struct
|
||||||
|
- file
|
||||||
|
- kv
|
||||||
|
- export
|
||||||
|
stylecheck:
|
||||||
|
dot-import-whitelist:
|
||||||
|
- github.com/yusing/go-proxy/internal/utils/testing # go tests only
|
||||||
|
- github.com/yusing/go-proxy/internal/api/v1/utils # api only
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: struct-tag
|
||||||
|
- name: blank-imports
|
||||||
|
- name: context-as-argument
|
||||||
|
- name: context-keys-type
|
||||||
|
- name: error-return
|
||||||
|
- name: error-strings
|
||||||
|
- name: error-naming
|
||||||
|
- name: exported
|
||||||
|
disabled: true
|
||||||
|
- name: if-return
|
||||||
|
- name: increment-decrement
|
||||||
|
- name: var-naming
|
||||||
|
- name: var-declaration
|
||||||
|
- name: package-comments
|
||||||
|
disabled: true
|
||||||
|
- name: range
|
||||||
|
- name: receiver-naming
|
||||||
|
- name: time-naming
|
||||||
|
- name: unexported-return
|
||||||
|
- name: indent-error-flow
|
||||||
|
- name: errorf
|
||||||
|
- name: empty-block
|
||||||
|
- name: superfluous-else
|
||||||
|
- name: unused-parameter
|
||||||
|
disabled: true
|
||||||
|
- name: unreachable-code
|
||||||
|
- name: redefines-builtin-id
|
||||||
|
gomoddirectives:
|
||||||
|
replace-allow-list:
|
||||||
|
- github.com/abbot/go-http-auth
|
||||||
|
- github.com/gorilla/mux
|
||||||
|
- github.com/mailgun/minheap
|
||||||
|
- github.com/mailgun/multibuf
|
||||||
|
- github.com/jaguilar/vt100
|
||||||
|
- github.com/cucumber/godog
|
||||||
|
- github.com/http-wasm/http-wasm-host-go
|
||||||
|
testifylint:
|
||||||
|
disable:
|
||||||
|
- suite-dont-use-pkg
|
||||||
|
- require-error
|
||||||
|
- go-require
|
||||||
|
staticcheck:
|
||||||
|
checks:
|
||||||
|
- all
|
||||||
|
- -SA1019
|
||||||
|
errcheck:
|
||||||
|
exclude-functions:
|
||||||
|
- fmt.Fprintln
|
||||||
linters:
|
linters:
|
||||||
default: all
|
enable-all: true
|
||||||
disable:
|
disable:
|
||||||
# - bodyclose
|
- execinquery # deprecated
|
||||||
- containedctx
|
- gomnd # deprecated
|
||||||
# - contextcheck
|
- sqlclosecheck # not relevant (SQL)
|
||||||
- cyclop
|
- rowserrcheck # not relevant (SQL)
|
||||||
- depguard
|
- cyclop # duplicate of gocyclo
|
||||||
# - dupl
|
- depguard # Not relevant
|
||||||
- err113
|
- nakedret # Too strict
|
||||||
- exhaustive
|
- lll # Not relevant
|
||||||
- exhaustruct
|
- gocyclo # must be fixed
|
||||||
- funcorder
|
- gocognit # Too strict
|
||||||
- forcetypeassert
|
- nestif # Too many false-positive.
|
||||||
- gochecknoglobals
|
- prealloc # Too many false-positive.
|
||||||
|
- makezero # Not relevant
|
||||||
|
- dupl # Too strict
|
||||||
|
- gci # I don't care
|
||||||
|
- goconst # Too annoying
|
||||||
|
- gosec # Too strict
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- gocognit
|
- gochecknoglobals
|
||||||
- goconst
|
- wsl # Too strict
|
||||||
- gocyclo
|
- nlreturn # Not relevant
|
||||||
- godot
|
- mnd # Too strict
|
||||||
- gomoddirectives
|
- testpackage # Too strict
|
||||||
- gosmopolitan
|
- tparallel # Not relevant
|
||||||
- ireturn
|
- paralleltest # Not relevant
|
||||||
- lll
|
- exhaustive # Not relevant
|
||||||
- maintidx
|
- exhaustruct # Not relevant
|
||||||
- makezero
|
- err113 # Too strict
|
||||||
- mnd
|
- wrapcheck # Too strict
|
||||||
- nakedret
|
- noctx # Too strict
|
||||||
- nestif
|
- bodyclose # too many false-positive
|
||||||
- nlreturn
|
- forcetypeassert # Too strict
|
||||||
- nonamedreturns
|
- tagliatelle # Too strict
|
||||||
- noinlineerr
|
- varnamelen # Not relevant
|
||||||
- paralleltest
|
- nilnil # Not relevant
|
||||||
- revive
|
- ireturn # Not relevant
|
||||||
- rowserrcheck
|
- contextcheck # too many false-positive
|
||||||
- sqlclosecheck
|
- containedctx # too many false-positive
|
||||||
- tagalign
|
- maintidx # kind of duplicate of gocyclo
|
||||||
- tagliatelle
|
- nonamedreturns # Too strict
|
||||||
- testpackage
|
- gosmopolitan # not relevant
|
||||||
- tparallel
|
- exportloopref # Not relevant since go1.22
|
||||||
- varnamelen
|
|
||||||
- wrapcheck
|
|
||||||
- wsl
|
|
||||||
- wsl_v5
|
|
||||||
settings:
|
|
||||||
errcheck:
|
|
||||||
exclude-functions:
|
|
||||||
- fmt.Fprintln
|
|
||||||
forbidigo:
|
|
||||||
forbid:
|
|
||||||
- pattern: ^print(ln)?$
|
|
||||||
funlen:
|
|
||||||
lines: -1
|
|
||||||
statements: 120
|
|
||||||
gocyclo:
|
|
||||||
min-complexity: 14
|
|
||||||
godox:
|
|
||||||
keywords:
|
|
||||||
- FIXME
|
|
||||||
gomoddirectives:
|
|
||||||
replace-allow-list:
|
|
||||||
- github.com/abbot/go-http-auth
|
|
||||||
- github.com/gorilla/mux
|
|
||||||
- github.com/mailgun/minheap
|
|
||||||
- github.com/mailgun/multibuf
|
|
||||||
- github.com/jaguilar/vt100
|
|
||||||
- github.com/cucumber/godog
|
|
||||||
- github.com/http-wasm/http-wasm-host-go
|
|
||||||
govet:
|
|
||||||
disable:
|
|
||||||
- shadow
|
|
||||||
- fieldalignment
|
|
||||||
enable-all: true
|
|
||||||
misspell:
|
|
||||||
locale: US
|
|
||||||
revive:
|
|
||||||
rules:
|
|
||||||
- name: struct-tag
|
|
||||||
- name: blank-imports
|
|
||||||
- name: context-as-argument
|
|
||||||
- name: context-keys-type
|
|
||||||
- name: error-return
|
|
||||||
- name: error-strings
|
|
||||||
- name: error-naming
|
|
||||||
- name: exported
|
|
||||||
disabled: true
|
|
||||||
- name: if-return
|
|
||||||
- name: increment-decrement
|
|
||||||
- name: var-naming
|
|
||||||
- name: var-declaration
|
|
||||||
- name: package-comments
|
|
||||||
disabled: true
|
|
||||||
- name: range
|
|
||||||
- name: receiver-naming
|
|
||||||
- name: time-naming
|
|
||||||
- name: unexported-return
|
|
||||||
- name: indent-error-flow
|
|
||||||
- name: errorf
|
|
||||||
- name: empty-block
|
|
||||||
- name: superfluous-else
|
|
||||||
- name: unused-parameter
|
|
||||||
disabled: true
|
|
||||||
- name: unreachable-code
|
|
||||||
- name: redefines-builtin-id
|
|
||||||
staticcheck:
|
|
||||||
checks:
|
|
||||||
- all
|
|
||||||
- -SA1019
|
|
||||||
dot-import-whitelist:
|
|
||||||
- github.com/yusing/go-proxy/internal/utils/testing
|
|
||||||
- github.com/yusing/go-proxy/internal/api/v1/utils
|
|
||||||
tagalign:
|
|
||||||
align: false
|
|
||||||
sort: true
|
|
||||||
order:
|
|
||||||
- description
|
|
||||||
- json
|
|
||||||
- toml
|
|
||||||
- yaml
|
|
||||||
- yml
|
|
||||||
- label
|
|
||||||
- label-slice-as-struct
|
|
||||||
- file
|
|
||||||
- kv
|
|
||||||
- export
|
|
||||||
testifylint:
|
|
||||||
disable:
|
|
||||||
- suite-dont-use-pkg
|
|
||||||
- require-error
|
|
||||||
- go-require
|
|
||||||
exclusions:
|
|
||||||
generated: lax
|
|
||||||
presets:
|
|
||||||
- comments
|
|
||||||
- common-false-positives
|
|
||||||
- legacy
|
|
||||||
- std-error-handling
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
formatters:
|
|
||||||
enable:
|
|
||||||
- gofmt
|
|
||||||
- gofumpt
|
|
||||||
- goimports
|
|
||||||
exclusions:
|
|
||||||
generated: lax
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
|
|||||||
@@ -2,37 +2,36 @@
|
|||||||
# 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.25.0
|
version: 1.22.10
|
||||||
# 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.7.2
|
ref: v1.6.7
|
||||||
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@22.16.0
|
- node@18.20.5
|
||||||
- python@3.10.8
|
- python@3.10.8
|
||||||
- go@1.24.3
|
- go@1.23.2
|
||||||
# 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.467
|
|
||||||
- golangci-lint2@2.4.0
|
|
||||||
- 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
|
||||||
- osv-scanner@2.2.2
|
- golangci-lint@1.64.5
|
||||||
- oxipng@9.1.5
|
- osv-scanner@1.9.2
|
||||||
- prettier@3.6.2
|
- oxipng@9.1.4
|
||||||
- shellcheck@0.11.0
|
- prettier@3.5.1
|
||||||
|
- shellcheck@0.10.0
|
||||||
- shfmt@3.6.0
|
- shfmt@3.6.0
|
||||||
- trufflehog@3.90.5
|
- trufflehog@3.88.9
|
||||||
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/godoxy-webui/raw/refs/heads/main/src/types/godoxy/config.schema.json": [
|
"https://github.com/yusing/go-proxy/raw/main/schemas/config.schema.json": [
|
||||||
"config.example.yml",
|
"config.example.yml",
|
||||||
"config.yml"
|
"config.yml"
|
||||||
],
|
],
|
||||||
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/routes.schema.json": [
|
"https://github.com/yusing/go-proxy/raw/main/schemas/routes.schema.json": [
|
||||||
"providers.example.yml"
|
"providers.example.yml"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
30
Dockerfile
30
Dockerfile
@@ -1,33 +1,30 @@
|
|||||||
# Stage 1: deps
|
# Stage 1: deps
|
||||||
FROM golang:1.25.1-alpine AS deps
|
FROM golang:1.24.2-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
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
# Only copy go.mod and go.sum initially for better caching
|
||||||
|
COPY go.mod go.sum /src/
|
||||||
|
|
||||||
# remove godoxy stuff from go.mod first
|
ENV GOPATH=/root/go
|
||||||
RUN sed -i '/^module github\.com\/yusing\/go-proxy/!{/github\.com\/yusing\/go-proxy/d}' go.mod && \
|
RUN go mod download -x
|
||||||
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
|
COPY migrations ./migrations
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
ENV VERSION=${VERSION}
|
ENV VERSION=${VERSION}
|
||||||
@@ -37,23 +34,24 @@ 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 && \
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
mv bin /app/ && \
|
||||||
--mount=type=cache,target=/root/go/pkg/mod \
|
mkdir -p /app/error_pages /app/certs
|
||||||
make ${MAKE_ARGS} docker=1 build
|
|
||||||
|
|
||||||
# Stage 3: Final image
|
# Stage 3: Final image
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|
||||||
LABEL maintainer="yusing@6uo.me"
|
LABEL maintainer="yusing@6uo.me"
|
||||||
LABEL proxy.exclude=1
|
LABEL proxy.exclude=1
|
||||||
LABEL proxy.#1.healthcheck.disable=true
|
|
||||||
|
|
||||||
# copy timezone data
|
# copy timezone data
|
||||||
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/run /app/run
|
COPY --from=builder /app /app
|
||||||
|
|
||||||
|
# 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
|
||||||
@@ -62,4 +60,4 @@ ENV DOCKER_HOST=unix:///var/run/docker.sock
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
CMD ["/app/run"]
|
CMD ["/app/run"]
|
||||||
|
|||||||
26
LICENSE
26
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 - present Yusing
|
Copyright (c) 2024 [fullname]
|
||||||
|
|
||||||
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,27 +19,3 @@ 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.
|
|
||||||
|
|||||||
108
Makefile
108
Makefile
@@ -1,22 +1,16 @@
|
|||||||
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
|
||||||
|
|
||||||
WEBUI_DIR ?= ../godoxy-frontend
|
LDFLAGS = -X github.com/yusing/go-proxy/pkg.version=${VERSION}
|
||||||
DOCS_DIR ?= ../godoxy-wiki
|
|
||||||
|
|
||||||
LDFLAGS = -X github.com/yusing/go-proxy/pkg.version=${VERSION} -checklinkname=0
|
|
||||||
|
|
||||||
ifeq ($(agent), 1)
|
ifeq ($(agent), 1)
|
||||||
NAME = godoxy-agent
|
NAME = godoxy-agent
|
||||||
PWD = ${shell pwd}/agent
|
CMD_PATH = ./agent/cmd
|
||||||
else ifeq ($(socket-proxy), 1)
|
|
||||||
NAME = godoxy-socket-proxy
|
|
||||||
PWD = ${shell pwd}/socket-proxy
|
|
||||||
else
|
else
|
||||||
NAME = godoxy
|
NAME = godoxy
|
||||||
PWD = ${shell pwd}
|
CMD_PATH = ./cmd
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(trace), 1)
|
ifeq ($(trace), 1)
|
||||||
@@ -31,9 +25,9 @@ ifeq ($(race), 1)
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(debug), 1)
|
ifeq ($(debug), 1)
|
||||||
CGO_ENABLED = 1
|
CGO_ENABLED = 0
|
||||||
GODOXY_DEBUG = 1
|
GODOXY_DEBUG = 1
|
||||||
BUILD_FLAGS += -gcflags=all='-N -l' -tags debug -asan
|
BUILD_FLAGS += -gcflags=all='-N -l' -tags debug
|
||||||
else ifeq ($(pprof), 1)
|
else ifeq ($(pprof), 1)
|
||||||
CGO_ENABLED = 1
|
CGO_ENABLED = 1
|
||||||
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
||||||
@@ -46,9 +40,9 @@ else
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
|
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
|
||||||
BIN_PATH := $(shell pwd)/bin/${NAME}
|
|
||||||
|
|
||||||
export NAME
|
export NAME
|
||||||
|
export CMD_PATH
|
||||||
export CGO_ENABLED
|
export CGO_ENABLED
|
||||||
export GODOXY_DEBUG
|
export GODOXY_DEBUG
|
||||||
export GODOXY_TRACE
|
export GODOXY_TRACE
|
||||||
@@ -56,74 +50,31 @@ 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
|
.PHONY: debug
|
||||||
|
|
||||||
test:
|
test:
|
||||||
GODOXY_TEST=1 go test ./internal/...
|
GODOXY_TEST=1 go test ./internal/...
|
||||||
|
|
||||||
docker-build-test:
|
get:
|
||||||
docker build -t godoxy .
|
go get -u ./cmd && go mod tidy
|
||||||
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 $(shell dirname ${BIN_PATH})
|
mkdir -p bin
|
||||||
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
|
go build ${BUILD_FLAGS} -o bin/${NAME} ${CMD_PATH}
|
||||||
${POST_BUILD}
|
if [ $(shell id -u) -eq 0 ]; \
|
||||||
|
then setcap CAP_NET_BIND_SERVICE=+eip bin/${NAME}; \
|
||||||
|
else sudo setcap CAP_NET_BIND_SERVICE=+eip bin/${NAME}; \
|
||||||
|
fi
|
||||||
|
|
||||||
run:
|
run:
|
||||||
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
|
[ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ${CMD_PATH}
|
||||||
|
|
||||||
dev:
|
debug:
|
||||||
docker compose -f dev.compose.yml up -t 0 -d
|
make NAME="godoxy-test" debug=1 build
|
||||||
|
sh -c 'HTTP_ADDR=:81 HTTPS_ADDR=:8443 API_ADDR=:8899 DEBUG=1 bin/godoxy-test'
|
||||||
dev-build: build
|
|
||||||
docker compose -f dev.compose.yml up -t 0 -d --build
|
|
||||||
|
|
||||||
dev-logs:
|
|
||||||
docker compose -f dev.compose.yml logs -f app
|
|
||||||
|
|
||||||
mtrace:
|
mtrace:
|
||||||
${BIN_PATH} debug-ls-mtrace > mtrace.json
|
bin/godoxy 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 &&\
|
||||||
@@ -138,21 +89,10 @@ 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 --include-lang=Go --not-match-f '_test.go$$' .
|
cloc --not-match-f '_test.go$$' cmd internal pkg
|
||||||
|
|
||||||
|
link-binary:
|
||||||
|
ln -s /app/${NAME} bin/run
|
||||||
|
|
||||||
push-github:
|
push-github:
|
||||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
|
|
||||||
gen-swagger:
|
|
||||||
swag init --parseDependency --parseInternal -g handler.go -d internal/api -o internal/api/v1/docs
|
|
||||||
python3 scripts/fix-swagger-json.py
|
|
||||||
# we don't need this
|
|
||||||
rm internal/api/v1/docs/docs.go
|
|
||||||
|
|
||||||
gen-swagger-markdown: gen-swagger
|
|
||||||
swagger generate markdown -f internal/api/v1/docs/swagger.yaml --skip-validation --output ${DOCS_DIR}/src/API.md
|
|
||||||
|
|
||||||
gen-api-types: gen-swagger
|
|
||||||
# --disable-throw-on-error
|
|
||||||
pnpx swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
|
||||||
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
|
||||||
161
README.md
161
README.md
@@ -1,121 +1,71 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img src="assets/godoxy.png" width="200">
|
# 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://sonarcloud.io/summary/new_code?id=yusing_godoxy)
|
||||||
|

|
||||||

|
|
||||||
[](https://discord.gg/umReR62nRd)
|
[](https://discord.gg/umReR62nRd)
|
||||||
|
|
||||||
A lightweight, simple, and performant reverse proxy with WebUI.
|
A lightweight, simple, and [performant](https://github.com/yusing/godoxy/wiki/Benchmarks) reverse proxy with WebUI.
|
||||||
|
|
||||||
<h5>
|
For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki)**
|
||||||
<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>
|
|
||||||
|
|
||||||
<h5>EN | <a href="README_CHT.md">中文</a></h5>
|
**EN** | <a href="README_CHT.md">中文</a>
|
||||||
|
|
||||||
<img src="screenshots/webui.jpg" style="max-width: 650">
|
<img src="screenshots/webui.jpg" style="max-width: 650">
|
||||||
|
|
||||||
Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)! (Thanks to [@ismesid](https://github.com/arevindh))
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Table of content
|
## Table of content
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
- [Table of content](#table-of-content)
|
- [GoDoxy](#godoxy)
|
||||||
- [Running demo](#running-demo)
|
- [Table of content](#table-of-content)
|
||||||
- [Key Features](#key-features)
|
- [Running demo](#running-demo)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Key Features](#key-features)
|
||||||
- [Setup](#setup)
|
- [Prerequisites](#prerequisites)
|
||||||
- [How does GoDoxy work](#how-does-godoxy-work)
|
- [How does GoDoxy work](#how-does-godoxy-work)
|
||||||
- [Screenshots](#screenshots)
|
- [Setup](#setup)
|
||||||
- [idlesleeper](#idlesleeper)
|
- [Screenshots](#screenshots)
|
||||||
- [Metrics and Logs](#metrics-and-logs)
|
- [idlesleeper](#idlesleeper)
|
||||||
- [Manual Setup](#manual-setup)
|
- [Metrics and Logs](#metrics-and-logs)
|
||||||
- [Folder structrue](#folder-structrue)
|
- [Manual Setup](#manual-setup)
|
||||||
- [Build it yourself](#build-it-yourself)
|
- [Folder structrue](#folder-structrue)
|
||||||
- [Star History](#star-history)
|
- [Build it yourself](#build-it-yourself)
|
||||||
|
|
||||||
## Running demo
|
## Running demo
|
||||||
|
|
||||||
<https://demo.godoxy.dev>
|
<https://godoxy.demo.6uo.me>
|
||||||
|
|
||||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- **Simple**
|
- Easy to use
|
||||||
- Effortless configuration with [simple labels](https://docs.godoxy.dev/Docker-labels-and-Route-Files) or WebUI
|
- Effortless configuration
|
||||||
- [Simple multi-node setup](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
|
- Simple multi-node setup with GoDoxy agents or Docker Socket Proxies
|
||||||
- Detailed error messages for easy troubleshooting.
|
- Error messages is clear and detailed, easy troubleshooting
|
||||||
- **ACL**: connection / request level access control
|
- **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))
|
||||||
- IP/CIDR
|
- **Auto hot-reload** on container state / config file changes
|
||||||
- Country **(Maxmind account required)**
|
- **Container aware**: create routes dynamically from running docker containers
|
||||||
- Timezone **(Maxmind account required)**
|
- **idlesleeper**: stop and wake containers based on traffic _(optional, see [screenshots](#idlesleeper))_
|
||||||
- **Access logging**
|
- HTTP reserve proxy and TCP/UDP port forwarding
|
||||||
- **Advanced Automation**
|
- **OpenID Connect integration**: SSO and secure your apps easily
|
||||||
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://docs.godoxy.dev/DNS-01-Providers))
|
- [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)
|
||||||
- Auto-configuration for Docker containers
|
- **Web UI with App dashboard, config editor, _uptime and system metrics_, _docker logs viewer_**
|
||||||
- Hot-reloading of configurations and container state changes
|
- Supports **linux/amd64** and **linux/arm64**
|
||||||
- **Container Runtime Support**
|
- Written in **[Go](https://go.dev)**
|
||||||
- Docker
|
|
||||||
- Podman
|
|
||||||
- **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
|
|
||||||
- **ForwardAuth support**: integrate with any auth provider (e.g. TinyAuth)
|
|
||||||
- **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)**
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
Setup Wildcard DNS Record(s) for machine running `GoDoxy`, e.g.
|
||||||
|
|
||||||
- A Record: `*.domain.com` -> `10.0.10.1`
|
- A Record: `*.domain.com` -> `10.0.10.1`
|
||||||
- AAAA Record (if you use IPv6): `*.domain.com` -> `::ffff:a00:a01`
|
- AAAA Record (if you use IPv6): `*.domain.com` -> `::ffff:a00:a01`
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> GoDoxy is designed to be running in `host` network mode, do not change it.
|
|
||||||
>
|
|
||||||
> To change listening ports, modify `.env`.
|
|
||||||
|
|
||||||
1. Prepare a new directory for docker compose and config files.
|
|
||||||
|
|
||||||
2. Run setup script inside the directory, or [set up manually](#manual-setup)
|
|
||||||
|
|
||||||
```shell
|
|
||||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Start the docker compose service from generated `compose.yml`:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
4. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
|
||||||
|
|
||||||
## How does GoDoxy work
|
## How does GoDoxy work
|
||||||
|
|
||||||
1. List all the containers
|
1. List all the containers
|
||||||
@@ -128,6 +78,23 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
|||||||
>
|
>
|
||||||
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> GoDoxy is designed to be running in `host` network mode, do not change it.
|
||||||
|
>
|
||||||
|
> To change listening ports, modify `.env`.
|
||||||
|
|
||||||
|
1. Prepare a new directory for docker compose and config files.
|
||||||
|
|
||||||
|
2. Run setup script inside the directory, or [set up manually](#manual-setup)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
### idlesleeper
|
### idlesleeper
|
||||||
@@ -139,12 +106,22 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><img src="screenshots/routes.jpg" alt="Routes" width="350"/></td>
|
<td align="center"><img src="screenshots/uptime.png" alt="Uptime Monitor" width="250"/></td>
|
||||||
<td align="center"><img src="screenshots/servers.jpg" alt="Servers" width="350"/></td>
|
<td align="center"><img src="screenshots/docker-logs.jpg" alt="Docker Logs" width="250"/></td>
|
||||||
|
<td align="center"><img src="screenshots/docker.jpg" alt="Server Overview" width="250"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><b>Routes</b></td>
|
<td align="center"><b>Uptime Monitor</b></td>
|
||||||
<td align="center"><b>Servers</b></td>
|
<td align="center"><b>Docker Logs</b></td>
|
||||||
|
<td align="center"><b>Server Overview</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><img src="screenshots/system-monitor.jpg" alt="System Monitor" width="250"/></td>
|
||||||
|
<td align="center"><img src="screenshots/system-info-graphs.jpg" alt="Graphs" width="250"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><b>System Monitor</b></td>
|
||||||
|
<td align="center"><b>Graphs</b></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,8 +173,4 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
|||||||
|
|
||||||
5. build binary with `make build`
|
5. build binary with `make build`
|
||||||
|
|
||||||
## Star History
|
|
||||||
|
|
||||||
[](https://www.star-history.com/#yusing/godoxy&Date)
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|||||||
127
README_CHT.md
127
README_CHT.md
@@ -1,89 +1,64 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img src="assets/godoxy.png" width="200">
|
# GoDoxy
|
||||||
|
|
||||||
[](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),且帶有主頁和配置面板的反向代理
|
||||||
|
|
||||||
<h5>
|
完整文檔請查閱 **[Wiki](https://github.com/yusing/godoxy/wiki)**(暫未有中文翻譯)
|
||||||
<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>
|
|
||||||
|
|
||||||
<h5><a href="README.md">EN</a> | 中文</h5>
|
<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">
|
||||||
|
|
||||||
有疑問? 問 [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)!(鳴謝 [@ismesid](https://github.com/arevindh))
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 目錄
|
## 目錄
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
- [目錄](#目錄)
|
- [GoDoxy](#godoxy)
|
||||||
- [運行示例](#運行示例)
|
- [目錄](#目錄)
|
||||||
- [主要特點](#主要特點)
|
- [運行示例](#運行示例)
|
||||||
- [前置需求](#前置需求)
|
- [主要特點](#主要特點)
|
||||||
- [安裝](#安裝)
|
- [前置需求](#前置需求)
|
||||||
- [手動安裝](#手動安裝)
|
- [安裝](#安裝)
|
||||||
- [資料夾結構](#資料夾結構)
|
- [手動安裝](#手動安裝)
|
||||||
- [截圖](#截圖)
|
- [資料夾結構](#資料夾結構)
|
||||||
- [閒置休眠](#閒置休眠)
|
- [截圖](#截圖)
|
||||||
- [監控](#監控)
|
- [閒置休眠](#閒置休眠)
|
||||||
- [自行編譯](#自行編譯)
|
- [監控](#監控)
|
||||||
- [Star History](#star-history)
|
- [自行編譯](#自行編譯)
|
||||||
|
|
||||||
## 運行示例
|
## 運行示例
|
||||||
|
|
||||||
<https://demo.godoxy.dev>
|
<https://godoxy.demo.6uo.me>
|
||||||
|
|
||||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||||
|
|
||||||
## 主要特點
|
## 主要特點
|
||||||
|
|
||||||
- **簡單易用**
|
- 容易使用
|
||||||
- 透過 Docker[標籤](https://docs.godoxy.dev/Docker-labels-and-Route-Files)或 WebUI 輕鬆設定
|
- 輕鬆配置
|
||||||
- [簡單的多節點設置](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
|
- 簡單的多節點設置
|
||||||
- 詳細的錯誤訊息,便於故障排除
|
- 錯誤訊息清晰詳細,易於排除故障
|
||||||
- **存取控制 (ACL)**:連線/請求層級存取控制
|
- 自動 SSL 憑證管理(參見 [支援的 DNS-01 驗證提供商](https://github.com/yusing/godoxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
||||||
- IP/CIDR
|
- 自動配置 Docker 容器
|
||||||
- 國家 **(需要 Maxmind 帳戶)**
|
- 容器狀態/配置文件變更時自動熱重載
|
||||||
- 時區 **(需要 Maxmind 帳戶)**
|
- **閒置休眠**:在閒置時停止容器,有流量時喚醒(_可選,參見[截圖](#閒置休眠)_)
|
||||||
- **存取日誌記錄**
|
- OpenID Connect:輕鬆實現單點登入
|
||||||
- **自動化**
|
- HTTP(s) 反向代理和TCP 和 UDP 埠轉發
|
||||||
- 使用 Let's Encrypt 自動管理 SSL 憑證 ([使用 DNS-01 驗證](https://docs.godoxy.dev/DNS-01-Providers))
|
- [HTTP 中介軟體](https://github.com/yusing/godoxy/wiki/Middlewares) 和 [自定義錯誤頁面](https://github.com/yusing/godoxy/wiki/Middlewares#custom-error-pages)
|
||||||
- Docker 容器自動配置
|
- **網頁介面,具有應用儀表板和配置編輯器**
|
||||||
- 設定檔與容器狀態變更時自動熱重載
|
- 支援 linux/amd64、linux/arm64
|
||||||
- **容器運行時支援**
|
- 使用 **[Go](https://go.dev)** 編寫
|
||||||
- Docker
|
|
||||||
- Podman
|
[🔼回到頂部](#目錄)
|
||||||
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
|
|
||||||
- Docker 容器
|
|
||||||
- Proxmox LXC 容器
|
|
||||||
- **流量管理**
|
|
||||||
- HTTP 反向代理
|
|
||||||
- TCP/UDP 連接埠轉送
|
|
||||||
- **OpenID Connect 支援**:輕鬆實現單點登入 (SSO) 並保護您的應用程式
|
|
||||||
- **ForwardAuth 支援**:整合任何 auth provider (例如 TinyAuth)
|
|
||||||
- **客製化**
|
|
||||||
- [HTTP 中介軟體](https://docs.godoxy.dev/Middlewares)
|
|
||||||
- [支援自訂錯誤頁面](https://docs.godoxy.dev/Custom-Error-Pages)
|
|
||||||
- **網頁使用者介面 (Web UI)**
|
|
||||||
- 應用程式一覽
|
|
||||||
- 設定編輯器
|
|
||||||
- 執行時間與系統指標
|
|
||||||
- Docker 日誌檢視器
|
|
||||||
- **跨平台支援**
|
|
||||||
- 支援 **linux/amd64** 與 **linux/arm64**
|
|
||||||
- **高效能**
|
|
||||||
- 以 **[Go](https://go.dev)** 語言編寫
|
|
||||||
|
|
||||||
## 前置需求
|
## 前置需求
|
||||||
|
|
||||||
@@ -103,12 +78,14 @@
|
|||||||
|
|
||||||
2. 在目錄內運行安裝腳本,或[手動安裝](#手動安裝)
|
2. 在目錄內運行安裝腳本,或[手動安裝](#手動安裝)
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
|
3. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
|
||||||
|
|
||||||
|
[🔼回到頂部](#目錄)
|
||||||
|
|
||||||
### 手動安裝
|
### 手動安裝
|
||||||
|
|
||||||
1. 建立 `config` 目錄,然後將 `config.example.yml` 下載到 `config/config.yml`
|
1. 建立 `config` 目錄,然後將 `config.example.yml` 下載到 `config/config.yml`
|
||||||
@@ -150,17 +127,29 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
[🔼回到頂部](#目錄)
|
||||||
|
|
||||||
### 監控
|
### 監控
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><img src="screenshots/routes.jpg" alt="Routes" width="350"/></td>
|
<td align="center"><img src="screenshots/uptime.png" alt="Uptime Monitor" width="250"/></td>
|
||||||
<td align="center"><img src="screenshots/servers.jpg" alt="Servers" width="350"/></td>
|
<td align="center"><img src="screenshots/docker-logs.jpg" alt="Docker Logs" width="250"/></td>
|
||||||
|
<td align="center"><img src="screenshots/docker.jpg" alt="Server Overview" width="250"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><b>路由</b></td>
|
<td align="center"><b>運行時間監控</b></td>
|
||||||
<td align="center"><b>伺服器</b></td>
|
<td align="center"><b>Docker 日誌</b></td>
|
||||||
|
<td align="center"><b>伺服器概覽</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><img src="screenshots/system-monitor.jpg" alt="System Monitor" width="250"/></td>
|
||||||
|
<td align="center"><img src="screenshots/system-info-graphs.jpg" alt="Graphs" width="250"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><b>系統監控</b></td>
|
||||||
|
<td align="center"><b>圖表</b></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,8 +166,4 @@
|
|||||||
|
|
||||||
5. 使用 `make build` 編譯二進制檔案
|
5. 使用 `make build` 編譯二進制檔案
|
||||||
|
|
||||||
## Star History
|
[🔼回到頂部](#目錄)
|
||||||
|
|
||||||
[](https://www.star-history.com/#yusing/godoxy&Date)
|
|
||||||
|
|
||||||
[🔼 回到頂部](#目錄)
|
|
||||||
|
|||||||
@@ -3,27 +3,20 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/env"
|
"github.com/yusing/go-proxy/agent/pkg/env"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/server"
|
"github.com/yusing/go-proxy/agent/pkg/server"
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging/memlogger"
|
||||||
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
||||||
httpServer "github.com/yusing/go-proxy/internal/net/gphttp/server"
|
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
"github.com/yusing/go-proxy/pkg"
|
"github.com/yusing/go-proxy/pkg"
|
||||||
socketproxy "github.com/yusing/go-proxy/socketproxy/pkg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
writer := zerolog.ConsoleWriter{
|
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
||||||
Out: os.Stderr,
|
|
||||||
TimeFormat: "01-02 15:04",
|
|
||||||
}
|
|
||||||
zerolog.TimeFieldFormat = writer.TimeFormat
|
|
||||||
log.Logger = zerolog.New(writer).Level(zerolog.InfoLevel).With().Timestamp().Logger()
|
|
||||||
ca := &agent.PEMPair{}
|
ca := &agent.PEMPair{}
|
||||||
err := ca.Load(env.AgentCACert)
|
err := ca.Load(env.AgentCACert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -44,12 +37,11 @@ func main() {
|
|||||||
gperr.LogFatal("init SSL error", err)
|
gperr.LogFatal("init SSL error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msgf("GoDoxy Agent version %s", pkg.GetVersion())
|
logging.Info().Msgf("GoDoxy Agent version %s", pkg.GetVersion())
|
||||||
log.Info().Msgf("Agent name: %s", env.AgentName)
|
logging.Info().Msgf("Agent name: %s", env.AgentName)
|
||||||
log.Info().Msgf("Agent port: %d", env.AgentPort)
|
logging.Info().Msgf("Agent port: %d", env.AgentPort)
|
||||||
log.Info().Msgf("Agent runtime: %s", env.Runtime)
|
|
||||||
|
|
||||||
log.Info().Msg(`
|
logging.Info().Msg(`
|
||||||
Tips:
|
Tips:
|
||||||
1. To change the agent name, you can set the AGENT_NAME environment variable.
|
1. To change the agent name, you can set the AGENT_NAME environment variable.
|
||||||
2. To change the agent port, you can set the AGENT_PORT environment variable.
|
2. To change the agent port, you can set the AGENT_PORT environment variable.
|
||||||
@@ -63,19 +55,6 @@ Tips:
|
|||||||
}
|
}
|
||||||
|
|
||||||
server.StartAgentServer(t, opts)
|
server.StartAgentServer(t, opts)
|
||||||
|
|
||||||
if socketproxy.ListenAddr != "" {
|
|
||||||
runtime := strutils.Title(string(env.Runtime))
|
|
||||||
|
|
||||||
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
|
|
||||||
opts := httpServer.Options{
|
|
||||||
Name: runtime,
|
|
||||||
HTTPAddr: socketproxy.ListenAddr,
|
|
||||||
Handler: socketproxy.NewHandler(),
|
|
||||||
}
|
|
||||||
httpServer.StartServer(t, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
systeminfo.Poller.Start()
|
systeminfo.Poller.Start()
|
||||||
|
|
||||||
task.WaitExit(3)
|
task.WaitExit(3)
|
||||||
|
|||||||
113
agent/go.mod
113
agent/go.mod
@@ -1,113 +0,0 @@
|
|||||||
module github.com/yusing/go-proxy/agent
|
|
||||||
|
|
||||||
go 1.25.1
|
|
||||||
|
|
||||||
replace github.com/yusing/go-proxy => ..
|
|
||||||
|
|
||||||
replace github.com/yusing/go-proxy/socketproxy => ../socket-proxy
|
|
||||||
|
|
||||||
replace github.com/yusing/go-proxy/internal/utils => ../internal/utils
|
|
||||||
|
|
||||||
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d
|
|
||||||
|
|
||||||
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/gin-gonic/gin v1.10.1
|
|
||||||
github.com/gorilla/websocket v1.5.3
|
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.1.0
|
|
||||||
github.com/rs/zerolog v1.34.0
|
|
||||||
github.com/stretchr/testify v1.11.1
|
|
||||||
github.com/yusing/go-proxy v0.17.6
|
|
||||||
github.com/yusing/go-proxy/internal/utils v0.0.0
|
|
||||||
github.com/yusing/go-proxy/socketproxy v0.0.0-00010101000000-000000000000
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
|
||||||
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/bytedance/gopkg v0.1.3 // indirect
|
|
||||||
github.com/bytedance/sonic v1.14.1 // indirect
|
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
|
||||||
github.com/docker/cli v28.4.0+incompatible // indirect
|
|
||||||
github.com/docker/docker v28.4.0+incompatible // indirect
|
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
|
||||||
github.com/ebitengine/purego v0.8.4 // indirect
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
|
||||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
|
||||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
|
||||||
github.com/gotify/server/v2 v2.7.2 // indirect
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.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-20250827001030-24949be3fa54 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
|
||||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 // 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/pelletier/go-toml/v2 v2.2.4 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
|
||||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
|
||||||
github.com/samber/lo v1.51.0 // indirect
|
|
||||||
github.com/samber/slog-common v0.19.0 // indirect
|
|
||||||
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
|
|
||||||
github.com/shirou/gopsutil/v4 v4.25.8 // indirect
|
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
|
||||||
github.com/vincent-petithory/dataurl v1.0.0 // indirect
|
|
||||||
github.com/yusing/ds v0.1.0 // indirect
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
|
||||||
go.opentelemetry.io/auto/sdk v1.2.0 // indirect
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
|
||||||
go.uber.org/mock v0.6.0 // indirect
|
|
||||||
golang.org/x/arch v0.21.0 // indirect
|
|
||||||
golang.org/x/crypto v0.42.0 // indirect
|
|
||||||
golang.org/x/mod v0.28.0 // indirect
|
|
||||||
golang.org/x/net v0.44.0 // indirect
|
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
|
||||||
golang.org/x/text v0.29.0 // indirect
|
|
||||||
golang.org/x/time v0.13.0 // indirect
|
|
||||||
golang.org/x/tools v0.37.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
|
||||||
)
|
|
||||||
311
agent/go.sum
311
agent/go.sum
@@ -1,311 +0,0 @@
|
|||||||
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/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
|
||||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
|
||||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
|
||||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
|
||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
|
||||||
github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY=
|
|
||||||
github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
|
||||||
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
|
|
||||||
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
|
||||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
|
||||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
|
||||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
|
||||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
|
||||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
|
||||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
|
||||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d h1:bNqtnmyhGDxpBSaFYIo7ferYRIc/QzlaGfIhh/JmMPk=
|
|
||||||
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d/go.mod h1:7iQ/w4jyGYJCZ56dZLNztwM4atNxj5C2HNTBxhLvV8A=
|
|
||||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
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.7.2 h1:YRgYl/kB7Uh4OINd7gK60N3QBTY4YEeKTrBLhd67LC4=
|
|
||||||
github.com/gotify/server/v2 v2.7.2/go.mod h1:KJH+8yhkAxArygPwaRfp9otHjt6tE0YSzdrsgPX/5EE=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
|
||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
|
||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/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-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
|
||||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
|
||||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
|
||||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
|
||||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
|
||||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
|
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
|
||||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
|
||||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
|
||||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
|
||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
|
||||||
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
|
|
||||||
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
|
||||||
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
|
||||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
|
||||||
github.com/samber/slog-zerolog/v2 v2.7.3 h1:/MkPDl/tJhijN2GvB1MWwBn2FU8RiL3rQ8gpXkQm2EY=
|
|
||||||
github.com/samber/slog-zerolog/v2 v2.7.3/go.mod h1:oWU7WHof4Xp8VguiNO02r1a4VzkgoOyOZhY5CuRke60=
|
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
|
||||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
|
||||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
|
||||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
|
||||||
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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
github.com/yusing/ds v0.1.0 h1:aiZs7jPMN3MEChUsddMYjpZFHhhAmkxrwRyIUnGy5AU=
|
|
||||||
github.com/yusing/ds v0.1.0/go.mod h1:KC785+mtt+Bau0LLR+slExDaUjeiqLT1k9Or6Rpryh4=
|
|
||||||
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.2.0 h1:YpRtUFjvhSymycLS2T81lT6IGhcUP+LUPtv0iv1N8bM=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.2.0/go.mod h1:1deq2zL7rwjwC8mR7XgY2N+tlIl6pjmEUoLDENMEzwk=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
|
||||||
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
|
|
||||||
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
|
||||||
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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
|
||||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
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.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
|
||||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
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.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
|
||||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
|
||||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
|
||||||
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.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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
|
||||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
|
||||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
|
||||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
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=
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"iter"
|
|
||||||
|
|
||||||
"github.com/puzpuzpuz/xsync/v4"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
var agentPool = xsync.NewMap[string, *AgentConfig](xsync.WithPresize(10))
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if common.IsTest {
|
|
||||||
agentPool.Store("test-agent", &AgentConfig{
|
|
||||||
Addr: "test-agent",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAgent(agentAddrOrDockerHost string) (*AgentConfig, bool) {
|
|
||||||
if !IsDockerHostAgent(agentAddrOrDockerHost) {
|
|
||||||
return getAgentByAddr(agentAddrOrDockerHost)
|
|
||||||
}
|
|
||||||
return getAgentByAddr(GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAgentByName(name string) (*AgentConfig, bool) {
|
|
||||||
for _, agent := range agentPool.Range {
|
|
||||||
if agent.Name == name {
|
|
||||||
return agent, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddAgent(agent *AgentConfig) {
|
|
||||||
agentPool.Store(agent.Addr, agent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveAgent(agent *AgentConfig) {
|
|
||||||
agentPool.Delete(agent.Addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveAllAgents() {
|
|
||||||
agentPool.Clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListAgents() []*AgentConfig {
|
|
||||||
agents := make([]*AgentConfig, 0, agentPool.Size())
|
|
||||||
for _, agent := range agentPool.Range {
|
|
||||||
agents = append(agents, agent)
|
|
||||||
}
|
|
||||||
return agents
|
|
||||||
}
|
|
||||||
|
|
||||||
func IterAgents() iter.Seq2[string, *AgentConfig] {
|
|
||||||
return agentPool.Range
|
|
||||||
}
|
|
||||||
|
|
||||||
func NumAgents() int {
|
|
||||||
return agentPool.Size()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAgentByAddr(addr string) (agent *AgentConfig, ok bool) {
|
|
||||||
agent, ok = agentPool.Load(addr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
16
agent/pkg/agent/agents.go
Normal file
16
agent/pkg/agent/agents.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/pool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type agents struct{ pool.Pool[*AgentConfig] }
|
||||||
|
|
||||||
|
var Agents = agents{pool.New[*AgentConfig]("agents")}
|
||||||
|
|
||||||
|
func (agents agents) Get(agentAddrOrDockerHost string) (*AgentConfig, bool) {
|
||||||
|
if !IsDockerHostAgent(agentAddrOrDockerHost) {
|
||||||
|
return agents.Base().Load(agentAddrOrDockerHost)
|
||||||
|
}
|
||||||
|
return agents.Base().Load(GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
|
||||||
|
}
|
||||||
@@ -10,17 +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}}" \
|
||||||
{{ if eq .ContainerRuntime "nerdctl" -}}
|
bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/go-proxy/main/scripts/install-agent.sh)"`
|
||||||
DOCKER_SOCKET="/var/run/containerd/containerd.sock" \
|
|
||||||
RUNTIME="nerdctl" \
|
|
||||||
{{ else if eq .ContainerRuntime "podman" -}}
|
|
||||||
DOCKER_SOCKET="/var/run/podman/podman.sock" \
|
|
||||||
RUNTIME="podman" \
|
|
||||||
{{ else -}}
|
|
||||||
DOCKER_SOCKET="/var/run/docker.sock" \
|
|
||||||
RUNTIME="docker" \
|
|
||||||
{{ end -}}
|
|
||||||
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))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -13,27 +11,23 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/certs"
|
"github.com/yusing/go-proxy/agent/pkg/certs"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
"github.com/yusing/go-proxy/pkg"
|
"github.com/yusing/go-proxy/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AgentConfig struct {
|
type AgentConfig struct {
|
||||||
Addr string `json:"addr"`
|
Addr string
|
||||||
Name string `json:"name"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Runtime ContainerRuntime `json:"runtime"`
|
|
||||||
|
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
l zerolog.Logger
|
name string
|
||||||
} // @name Agent
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EndpointVersion = "/version"
|
EndpointVersion = "/version"
|
||||||
EndpointName = "/name"
|
EndpointName = "/name"
|
||||||
EndpointRuntime = "/runtime"
|
|
||||||
EndpointProxyHTTP = "/proxy/http"
|
EndpointProxyHTTP = "/proxy/http"
|
||||||
EndpointHealth = "/health"
|
EndpointHealth = "/health"
|
||||||
EndpointLogs = "/logs"
|
EndpointLogs = "/logs"
|
||||||
@@ -50,20 +44,21 @@ 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 = mustParseURL(APIBaseURL)
|
AgentURL, _ = url.Parse(APIBaseURL)
|
||||||
HTTPProxyURL = mustParseURL(APIBaseURL + EndpointProxyHTTP)
|
HTTPProxyURL, _ = url.Parse(APIBaseURL + EndpointProxyHTTP)
|
||||||
HTTPProxyURLPrefixLen = len(APIEndpointBase + EndpointProxyHTTP)
|
HTTPProxyURLPrefixLen = len(APIEndpointBase + EndpointProxyHTTP)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TestAgentConfig is a helper function to create an AgentConfig for testing purposes.
|
||||||
|
// Not used in production.
|
||||||
|
func TestAgentConfig(name string, addr string) *AgentConfig {
|
||||||
|
return &AgentConfig{
|
||||||
|
name: name,
|
||||||
|
Addr: addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func IsDockerHostAgent(dockerHost string) bool {
|
func IsDockerHostAgent(dockerHost string) bool {
|
||||||
return strings.HasPrefix(dockerHost, FakeDockerHostPrefix)
|
return strings.HasPrefix(dockerHost, FakeDockerHostPrefix)
|
||||||
}
|
}
|
||||||
@@ -72,6 +67,11 @@ func GetAgentAddrFromDockerHost(dockerHost string) string {
|
|||||||
return dockerHost[FakeDockerHostPrefixLen:]
|
return dockerHost[FakeDockerHostPrefixLen:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Key implements pool.Object
|
||||||
|
func (cfg *AgentConfig) Key() string {
|
||||||
|
return cfg.Addr
|
||||||
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) FakeDockerHost() string {
|
func (cfg *AgentConfig) FakeDockerHost() string {
|
||||||
return FakeDockerHostPrefix + cfg.Addr
|
return FakeDockerHostPrefix + cfg.Addr
|
||||||
}
|
}
|
||||||
@@ -81,9 +81,7 @@ func (cfg *AgentConfig) Parse(addr string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverVersion = pkg.GetVersion()
|
func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte) error {
|
||||||
|
|
||||||
func (cfg *AgentConfig) StartWithCerts(ctx context.Context, 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
|
||||||
@@ -93,7 +91,7 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
|
|||||||
caCertPool := x509.NewCertPool()
|
caCertPool := x509.NewCertPool()
|
||||||
ok := caCertPool.AppendCertsFromPEM(ca)
|
ok := caCertPool.AppendCertsFromPEM(ca)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("invalid ca certificate")
|
return gperr.New("invalid ca certificate")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.tlsConfig = &tls.Config{
|
cfg.tlsConfig = &tls.Config{
|
||||||
@@ -108,74 +106,45 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
|
|||||||
ctx, cancel := context.WithTimeout(ctx, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
agentVer := pkg.ParseVersion(string(version))
|
||||||
|
serverVer := pkg.GetVersion()
|
||||||
|
if !agentVer.IsEqual(serverVer) {
|
||||||
|
return gperr.Errorf("agent version mismatch: server: %s, agent: %s", serverVer, agentVer)
|
||||||
|
}
|
||||||
|
|
||||||
// get agent name
|
// get agent name
|
||||||
name, _, err := cfg.Fetch(ctx, EndpointName)
|
name, _, err := cfg.Fetch(ctx, EndpointName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Name = string(name)
|
cfg.name = string(name)
|
||||||
|
|
||||||
cfg.l = log.With().Str("agent", cfg.Name).Logger()
|
|
||||||
|
|
||||||
// check agent version
|
|
||||||
agentVersionBytes, _, err := cfg.Fetch(ctx, EndpointVersion)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check agent runtime
|
|
||||||
runtimeBytes, status, err := cfg.Fetch(ctx, EndpointRuntime)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch status {
|
|
||||||
case http.StatusOK:
|
|
||||||
switch string(runtimeBytes) {
|
|
||||||
case "docker":
|
|
||||||
cfg.Runtime = ContainerRuntimeDocker
|
|
||||||
// case "nerdctl":
|
|
||||||
// cfg.Runtime = ContainerRuntimeNerdctl
|
|
||||||
case "podman":
|
|
||||||
cfg.Runtime = ContainerRuntimePodman
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid agent runtime: %s", runtimeBytes)
|
|
||||||
}
|
|
||||||
case http.StatusNotFound:
|
|
||||||
// backward compatibility, old agent does not have runtime endpoint
|
|
||||||
cfg.Runtime = ContainerRuntimeDocker
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("failed to get agent runtime: HTTP %d %s", status, runtimeBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Version = string(agentVersionBytes)
|
|
||||||
agentVersion := pkg.ParseVersion(cfg.Version)
|
|
||||||
|
|
||||||
if serverVersion.IsNewerMajorThan(agentVersion) {
|
|
||||||
log.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.Name, serverVersion, agentVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msgf("agent %q initialized", cfg.Name)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) Start(ctx context.Context) error {
|
func (cfg *AgentConfig) Init(ctx context.Context) gperr.Error {
|
||||||
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
|
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid agent host: %s", cfg.Addr)
|
return gperr.New("invalid agent host").Subject(cfg.Addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
certData, err := os.ReadFile(filepath)
|
certData, err := os.ReadFile(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read agent certs: %w", err)
|
return gperr.Wrap(err, "failed to read agent certs")
|
||||||
}
|
}
|
||||||
|
|
||||||
ca, crt, key, err := certs.ExtractCert(certData)
|
ca, crt, key, err := certs.ExtractCert(certData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to extract agent certs: %w", err)
|
return gperr.Wrap(err, "failed to extract agent certs")
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg.StartWithCerts(ctx, ca, crt, key)
|
return gperr.Wrap(cfg.InitWithCerts(ctx, ca, crt, key))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) NewHTTPClient() *http.Client {
|
func (cfg *AgentConfig) NewHTTPClient() *http.Client {
|
||||||
@@ -199,12 +168,26 @@ 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 dialer.DialContext(ctx, "tcp", cfg.Addr)
|
return gphttp.DefaultDialer.DialContext(ctx, "tcp", cfg.Addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) IsInitialized() bool {
|
||||||
|
return cfg.name != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) Name() string {
|
||||||
|
return cfg.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) String() string {
|
func (cfg *AgentConfig) String() string {
|
||||||
return cfg.Name + "@" + cfg.Addr
|
return cfg.name + "@" + cfg.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalMap implements pool.Object
|
||||||
|
func (cfg *AgentConfig) MarshalMap() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"name": cfg.Name(),
|
||||||
|
"addr": cfg.Addr,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed templates/agent.compose.yml.tmpl
|
//go:embed templates/agent.compose.yml
|
||||||
agentComposeYAML string
|
agentComposeYAML string
|
||||||
agentComposeYAMLTemplate = template.Must(template.New("agent.compose.yml.tmpl").Parse(agentComposeYAML))
|
agentComposeYAMLTemplate = template.Must(template.New("agent.compose.yml").Parse(agentComposeYAML))
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -20,8 +20,7 @@ const (
|
|||||||
|
|
||||||
func (c *AgentComposeConfig) Generate() (string, error) {
|
func (c *AgentComposeConfig) Generate() (string, error) {
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||||
err := agentComposeYAMLTemplate.Execute(buf, c)
|
if err := agentComposeYAMLTemplate.Execute(buf, c); err != nil {
|
||||||
if err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return buf.String(), nil
|
return buf.String(), nil
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
type (
|
type (
|
||||||
ContainerRuntime string
|
AgentEnvConfig struct {
|
||||||
AgentEnvConfig struct {
|
Name string
|
||||||
Name string
|
Port int
|
||||||
Port int
|
CACert string
|
||||||
CACert string
|
SSLCert string
|
||||||
SSLCert string
|
|
||||||
ContainerRuntime ContainerRuntime
|
|
||||||
}
|
}
|
||||||
AgentComposeConfig struct {
|
AgentComposeConfig struct {
|
||||||
Image string
|
Image string
|
||||||
@@ -17,9 +15,3 @@ type (
|
|||||||
Generate() (string, error)
|
Generate() (string, error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
ContainerRuntimeDocker ContainerRuntime = "docker"
|
|
||||||
ContainerRuntimePodman ContainerRuntime = "podman"
|
|
||||||
// ContainerRuntimeNerdctl ContainerRuntime = "nerdctl"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
|
||||||
nettypes "github.com/yusing/go-proxy/internal/net/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cfg *AgentConfig) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, method, APIBaseURL+endpoint, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return cfg.httpClient.Do(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) (*http.Response, error) {
|
|
||||||
req = req.WithContext(req.Context())
|
|
||||||
req.URL.Host = AgentHost
|
|
||||||
req.URL.Scheme = "https"
|
|
||||||
req.URL.Path = APIEndpointBase + endpoint
|
|
||||||
req.RequestURI = ""
|
|
||||||
resp, err := cfg.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *AgentConfig) Fetch(ctx context.Context, endpoint string) ([]byte, int, error) {
|
|
||||||
resp, err := cfg.Do(ctx, "GET", endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
data, _ := io.ReadAll(resp.Body)
|
|
||||||
return data, resp.StatusCode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *AgentConfig) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
|
||||||
transport := cfg.Transport()
|
|
||||||
dialer := websocket.Dialer{
|
|
||||||
NetDialContext: transport.DialContext,
|
|
||||||
NetDialTLSContext: transport.DialTLSContext,
|
|
||||||
}
|
|
||||||
return dialer.DialContext(ctx, APIBaseURL+endpoint, http.Header{
|
|
||||||
"Host": {AgentHost},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReverseProxy reverse proxies the request to the agent
|
|
||||||
//
|
|
||||||
// It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint
|
|
||||||
// If the request has a query, it will be added to the proxy request's URL
|
|
||||||
func (cfg *AgentConfig) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) error {
|
|
||||||
rp := reverseproxy.NewReverseProxy("agent", nettypes.NewURL(AgentURL), cfg.Transport())
|
|
||||||
uri := APIEndpointBase + endpoint
|
|
||||||
if req.URL.RawQuery != "" {
|
|
||||||
uri += "?" + req.URL.RawQuery
|
|
||||||
}
|
|
||||||
r, err := http.NewRequestWithContext(req.Context(), req.Method, uri, req.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
r.Header = req.Header
|
|
||||||
rp.ServeHTTP(w, r)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,50 +1,31 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"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 *ecdsa.PrivateKey) *PEMPair {
|
func toPEMPair(certDER []byte, key *rsa.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: "EC PRIVATE KEY", Bytes: marshaledKey}),
|
Key: pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -77,84 +58,15 @@ func (p *PEMPair) Load(data string) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PEMPair) Encrypt(encKey []byte) (PEMPair, error) {
|
|
||||||
cert, err := encrypt(p.Cert, encKey)
|
|
||||||
if err != nil {
|
|
||||||
return PEMPair{}, err
|
|
||||||
}
|
|
||||||
key, err := encrypt(p.Key, encKey)
|
|
||||||
if err != nil {
|
|
||||||
return PEMPair{}, err
|
|
||||||
}
|
|
||||||
return PEMPair{Cert: cert, Key: key}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PEMPair) Decrypt(encKey []byte) (PEMPair, error) {
|
|
||||||
cert, err := decrypt(p.Cert, encKey)
|
|
||||||
if err != nil {
|
|
||||||
return PEMPair{}, err
|
|
||||||
}
|
|
||||||
key, err := decrypt(p.Key, encKey)
|
|
||||||
if err != nil {
|
|
||||||
return PEMPair{}, err
|
|
||||||
}
|
|
||||||
return PEMPair{Cert: cert, Key: key}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func encrypt(data []byte, key []byte) ([]byte, error) {
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
gcm, err := cipher.NewGCM(block)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
nonce := make([]byte, gcm.NonceSize())
|
|
||||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return gcm.Seal(nonce, nonce, data, nil), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decrypt(data []byte, key []byte) ([]byte, error) {
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
gcm, err := cipher.NewGCM(block)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
nonce := data[:gcm.NonceSize()]
|
|
||||||
ciphertext := data[gcm.NonceSize():]
|
|
||||||
return gcm.Open(nil, nonce, ciphertext, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PEMPair) ToTLSCert() (*tls.Certificate, error) {
|
func (p *PEMPair) ToTLSCert() (*tls.Certificate, error) {
|
||||||
cert, err := tls.X509KeyPair(p.Cert, p.Key)
|
cert, err := tls.X509KeyPair(p.Cert, p.Key)
|
||||||
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: caSerialNumber,
|
SerialNumber: big.NewInt(1),
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
Organization: []string{"GoDoxy"},
|
Organization: []string{"GoDoxy"},
|
||||||
CommonName: CertsDNSName,
|
CommonName: CertsDNSName,
|
||||||
@@ -164,12 +76,9 @@ 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 := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
caKey, err := rsa.GenerateKey(rand.Reader, KeySize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -182,29 +91,20 @@ 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 := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
serverKey, err := rsa.GenerateKey(rand.Reader, KeySize)
|
||||||
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: serverSerialNumber,
|
SerialNumber: big.NewInt(2),
|
||||||
Issuer: caTemplate.Subject,
|
Issuer: caTemplate.Subject,
|
||||||
Subject: pkix.Name{
|
Subject: caTemplate.Subject,
|
||||||
Organization: caTemplate.Subject.Organization,
|
DNSNames: []string{CertsDNSName},
|
||||||
OrganizationalUnit: []string{"Server"},
|
NotBefore: time.Now(),
|
||||||
CommonName: CertsDNSName,
|
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
|
||||||
},
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
DNSNames: []string{CertsDNSName},
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
||||||
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)
|
||||||
@@ -214,29 +114,20 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
|||||||
|
|
||||||
srv = toPEMPair(srvCertDER, serverKey)
|
srv = toPEMPair(srvCertDER, serverKey)
|
||||||
|
|
||||||
clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
clientKey, err := rsa.GenerateKey(rand.Reader, KeySize)
|
||||||
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: clientSerialNumber,
|
SerialNumber: big.NewInt(3),
|
||||||
Issuer: caTemplate.Subject,
|
Issuer: caTemplate.Subject,
|
||||||
Subject: pkix.Name{
|
Subject: caTemplate.Subject,
|
||||||
Organization: caTemplate.Subject.Organization,
|
DNSNames: []string{CertsDNSName},
|
||||||
OrganizationalUnit: []string{"Client"},
|
NotBefore: time.Now(),
|
||||||
CommonName: CertsDNSName,
|
NotAfter: time.Now().AddDate(1000, 0, 0),
|
||||||
},
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
DNSNames: []string{CertsDNSName},
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().AddDate(1000, 0, 0),
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -9,59 +8,59 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewAgent(t *testing.T) {
|
func TestNewAgent(t *testing.T) {
|
||||||
ca, srv, client, err := NewAgent()
|
ca, srv, client, err := NewAgent()
|
||||||
require.NoError(t, err)
|
ExpectNoError(t, err)
|
||||||
require.NotNil(t, ca)
|
ExpectTrue(t, ca != nil)
|
||||||
require.NotNil(t, srv)
|
ExpectTrue(t, srv != nil)
|
||||||
require.NotNil(t, client)
|
ExpectTrue(t, client != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPEMPair(t *testing.T) {
|
func TestPEMPair(t *testing.T) {
|
||||||
ca, srv, client, err := NewAgent()
|
ca, srv, client, err := NewAgent()
|
||||||
require.NoError(t, err)
|
ExpectNoError(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())
|
||||||
require.NoError(t, err)
|
ExpectNoError(t, err)
|
||||||
require.Equal(t, p.Cert, pp.Cert)
|
ExpectEqual(t, p.Cert, pp.Cert)
|
||||||
require.Equal(t, p.Key, pp.Key)
|
ExpectEqual(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()
|
||||||
require.NoError(t, err)
|
ExpectNoError(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()
|
||||||
require.NoError(t, err)
|
ExpectNoError(t, err)
|
||||||
require.NotNil(t, cert)
|
ExpectTrue(t, cert != nil)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerClient(t *testing.T) {
|
func TestServerClient(t *testing.T) {
|
||||||
ca, srv, client, err := NewAgent()
|
ca, srv, client, err := NewAgent()
|
||||||
require.NoError(t, err)
|
ExpectNoError(t, err)
|
||||||
|
|
||||||
srvTLS, err := srv.ToTLSCert()
|
srvTLS, err := srv.ToTLSCert()
|
||||||
require.NoError(t, err)
|
ExpectNoError(t, err)
|
||||||
require.NotNil(t, srvTLS)
|
ExpectTrue(t, srvTLS != nil)
|
||||||
|
|
||||||
clientTLS, err := client.ToTLSCert()
|
clientTLS, err := client.ToTLSCert()
|
||||||
require.NoError(t, err)
|
ExpectNoError(t, err)
|
||||||
require.NotNil(t, clientTLS)
|
ExpectTrue(t, clientTLS != nil)
|
||||||
|
|
||||||
caPool := x509.NewCertPool()
|
caPool := x509.NewCertPool()
|
||||||
require.True(t, caPool.AppendCertsFromPEM(ca.Cert))
|
ExpectTrue(t, caPool.AppendCertsFromPEM(ca.Cert))
|
||||||
|
|
||||||
srvTLSConfig := &tls.Config{
|
srvTLSConfig := &tls.Config{
|
||||||
Certificates: []tls.Certificate{*srvTLS},
|
Certificates: []tls.Certificate{*srvTLS},
|
||||||
@@ -87,26 +86,6 @@ func TestServerClient(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := httpClient.Get(server.URL)
|
resp, err := httpClient.Get(server.URL)
|
||||||
require.NoError(t, err)
|
ExpectNoError(t, err)
|
||||||
require.Equal(t, resp.StatusCode, http.StatusOK)
|
ExpectEqual(t, resp.StatusCode, http.StatusOK)
|
||||||
}
|
|
||||||
|
|
||||||
func TestPEMPairEncryptDecrypt(t *testing.T) {
|
|
||||||
encKey := make([]byte, 32)
|
|
||||||
_, err := rand.Read(encKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ca, _, _, err := NewAgent()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
encCA, err := ca.Encrypt(encKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, encCA)
|
|
||||||
|
|
||||||
decCA, err := encCA.Decrypt(encKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, decCA)
|
|
||||||
|
|
||||||
require.Equal(t, string(ca.Cert), string(decCA.Cert))
|
|
||||||
require.Equal(t, string(ca.Key), string(decCA.Key))
|
|
||||||
}
|
}
|
||||||
|
|||||||
49
agent/pkg/agent/requests.go
Normal file
49
agent/pkg/agent/requests.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, APIBaseURL+endpoint, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cfg.httpClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) ([]byte, int, error) {
|
||||||
|
req = req.WithContext(req.Context())
|
||||||
|
req.URL.Host = AgentHost
|
||||||
|
req.URL.Scheme = "https"
|
||||||
|
req.URL.Path = APIEndpointBase + endpoint
|
||||||
|
req.RequestURI = ""
|
||||||
|
resp, err := cfg.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
data, _ := io.ReadAll(resp.Body)
|
||||||
|
return data, resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) Fetch(ctx context.Context, endpoint string) ([]byte, int, error) {
|
||||||
|
resp, err := cfg.Do(ctx, "GET", endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
data, _ := io.ReadAll(resp.Body)
|
||||||
|
return data, resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
||||||
|
return websocket.Dial(ctx, APIBaseURL+endpoint, &websocket.DialOptions{
|
||||||
|
HTTPClient: cfg.NewHTTPClient(),
|
||||||
|
Host: AgentHost,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -9,36 +9,6 @@ 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
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
services:
|
|
||||||
agent:
|
|
||||||
image: "{{.Image}}"
|
|
||||||
container_name: godoxy-agent
|
|
||||||
restart: always
|
|
||||||
{{ if eq .ContainerRuntime "podman" -}}
|
|
||||||
ports:
|
|
||||||
- "{{.Port}}:{{.Port}}"
|
|
||||||
{{ else -}}
|
|
||||||
network_mode: host # do not change this
|
|
||||||
{{ end -}}
|
|
||||||
environment:
|
|
||||||
{{ if eq .ContainerRuntime "nerdctl" -}}
|
|
||||||
DOCKER_SOCKET: "/var/run/containerd/containerd.sock"
|
|
||||||
RUNTIME: "nerdctl"
|
|
||||||
{{ else if eq .ContainerRuntime "podman" -}}
|
|
||||||
DOCKER_SOCKET: "/var/run/podman/podman.sock"
|
|
||||||
RUNTIME: "podman"
|
|
||||||
{{ else -}}
|
|
||||||
DOCKER_SOCKET: "/var/run/docker.sock"
|
|
||||||
RUNTIME: "docker"
|
|
||||||
{{ end -}}
|
|
||||||
AGENT_NAME: "{{.Name}}"
|
|
||||||
AGENT_PORT: "{{.Port}}"
|
|
||||||
AGENT_CA_CERT: "{{.CACert}}"
|
|
||||||
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:
|
|
||||||
{{ if eq .ContainerRuntime "podman" -}}
|
|
||||||
- /var/run/podman/podman.sock:/var/run/podman/podman.sock
|
|
||||||
{{ else if eq .ContainerRuntime "nerdctl" -}}
|
|
||||||
- /var/run/containerd/containerd.sock:/var/run/containerd/containerd.sock
|
|
||||||
- /var/lib/nerdctl:/var/lib/nerdctl:ro # required to read metadata like network info
|
|
||||||
{{ else -}}
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
{{ end -}}
|
|
||||||
- ./data:/app/data
|
|
||||||
@@ -6,11 +6,10 @@ 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,
|
||||||
@@ -60,7 +59,7 @@ func AgentCertsFilepath(host string) (filepathOut string, ok bool) {
|
|||||||
if !isValidAgentHost(host) {
|
if !isValidAgentHost(host) {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
return filepath.Join(AgentCertsBasePath, host+".zip"), true
|
return filepath.Join(common.CertsDir, host+".zip"), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExtractCert(data []byte) (ca, crt, key []byte, err error) {
|
func ExtractCert(data []byte) (ca, crt, key []byte, err error) {
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
package certs_test
|
package certs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
"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 := certs.ZipCert(ca, crt, key)
|
zipData, err := ZipCert(ca, crt, key)
|
||||||
require.NoError(t, err)
|
ExpectNoError(t, err)
|
||||||
|
|
||||||
ca2, crt2, key2, err := certs.ExtractCert(zipData)
|
ca2, crt2, key2, err := ExtractCert(zipData)
|
||||||
require.NoError(t, err)
|
ExpectNoError(t, err)
|
||||||
require.Equal(t, ca, ca2)
|
ExpectEqual(t, ca, ca2)
|
||||||
require.Equal(t, crt, crt2)
|
ExpectEqual(t, crt, crt2)
|
||||||
require.Equal(t, key, key2)
|
ExpectEqual(t, key, key2)
|
||||||
}
|
}
|
||||||
|
|||||||
33
agent/pkg/env/env.go
vendored
33
agent/pkg/env/env.go
vendored
@@ -3,10 +3,7 @@ package env
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func DefaultAgentName() string {
|
func DefaultAgentName() string {
|
||||||
@@ -18,32 +15,10 @@ func DefaultAgentName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
AgentName string
|
AgentName = common.GetEnvString("AGENT_NAME", DefaultAgentName())
|
||||||
AgentPort int
|
AgentPort = common.GetEnvInt("AGENT_PORT", 8890)
|
||||||
AgentSkipClientCertCheck bool
|
|
||||||
AgentCACert string
|
|
||||||
AgentSSLCert string
|
|
||||||
DockerSocket string
|
|
||||||
Runtime agent.ContainerRuntime
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Load() {
|
|
||||||
DockerSocket = common.GetEnvString("DOCKER_SOCKET", "/var/run/docker.sock")
|
|
||||||
AgentName = common.GetEnvString("AGENT_NAME", DefaultAgentName())
|
|
||||||
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", "")
|
||||||
Runtime = agent.ContainerRuntime(common.GetEnvString("RUNTIME", "docker"))
|
)
|
||||||
|
|
||||||
switch Runtime {
|
|
||||||
case agent.ContainerRuntimeDocker, agent.ContainerRuntimePodman: //, agent.ContainerRuntimeNerdctl:
|
|
||||||
default:
|
|
||||||
log.Fatal().Str("runtime", string(Runtime)).Msg("invalid runtime")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
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/types"
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultHealthConfig = types.DefaultHealthConfig()
|
var defaultHealthConfig = health.DefaultHealthConfig()
|
||||||
|
|
||||||
func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
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, "missing scheme", http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var result *types.HealthCheckResult
|
var result *health.HealthCheckResult
|
||||||
var err error
|
var err error
|
||||||
switch scheme {
|
switch scheme {
|
||||||
case "fileserver":
|
case "fileserver":
|
||||||
path := query.Get("path")
|
path := query.Get("path")
|
||||||
if path == "" {
|
if path == "" {
|
||||||
http.Error(w, "missing path", http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
result = &types.HealthCheckResult{Healthy: err == nil}
|
result = &health.HealthCheckResult{Healthy: err == nil}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Detail = err.Error()
|
result.Detail = err.Error()
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
host := query.Get("host")
|
host := query.Get("host")
|
||||||
path := query.Get("path")
|
path := query.Get("path")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
http.Error(w, "missing host", http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err = monitor.NewHTTPHealthMonitor(&url.URL{
|
result, err = monitor.NewHTTPHealthMonitor(&url.URL{
|
||||||
@@ -51,17 +51,16 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
case "tcp", "udp":
|
case "tcp", "udp":
|
||||||
host := query.Get("host")
|
host := query.Get("host")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
http.Error(w, "missing host", http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), 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 {
|
||||||
http.Error(w, "port and host with port cannot both be provided", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if port != "" {
|
|
||||||
host = fmt.Sprintf("%s:%s", host, port)
|
host = fmt.Sprintf("%s:%s", host, port)
|
||||||
|
} else {
|
||||||
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
result, err = monitor.NewRawHealthMonitor(&url.URL{
|
result, err = monitor.NewRawHealthMonitor(&url.URL{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
@@ -74,7 +73,5 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
gphttp.RespondJSON(w, r, result)
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
json.NewEncoder(w).Encode(result)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package handler_test
|
package handler_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -9,10 +8,12 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/pkg/json"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/handler"
|
"github.com/yusing/go-proxy/agent/pkg/handler"
|
||||||
"github.com/yusing/go-proxy/internal/types"
|
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCheckHealthHTTP(t *testing.T) {
|
func TestCheckHealthHTTP(t *testing.T) {
|
||||||
@@ -81,7 +82,7 @@ func TestCheckHealthHTTP(t *testing.T) {
|
|||||||
require.Equal(t, recorder.Code, tt.expectedStatus)
|
require.Equal(t, recorder.Code, tt.expectedStatus)
|
||||||
|
|
||||||
if tt.expectedStatus == http.StatusOK {
|
if tt.expectedStatus == http.StatusOK {
|
||||||
var result types.HealthCheckResult
|
var result health.HealthCheckResult
|
||||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
||||||
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
||||||
}
|
}
|
||||||
@@ -125,7 +126,7 @@ func TestCheckHealthFileServer(t *testing.T) {
|
|||||||
|
|
||||||
require.Equal(t, recorder.Code, tt.expectedStatus)
|
require.Equal(t, recorder.Code, tt.expectedStatus)
|
||||||
|
|
||||||
var result types.HealthCheckResult
|
var result health.HealthCheckResult
|
||||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
||||||
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
||||||
require.Equal(t, result.Detail, tt.expectedDetail)
|
require.Equal(t, result.Detail, tt.expectedDetail)
|
||||||
@@ -172,9 +173,9 @@ func TestCheckHealthTCPUDP(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "InvalidHost",
|
name: "InvalidHost",
|
||||||
scheme: "tcp",
|
scheme: "tcp",
|
||||||
host: "",
|
host: "invalid",
|
||||||
port: 8080,
|
port: 8080,
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusOK,
|
||||||
expectedHealthy: false,
|
expectedHealthy: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -188,17 +189,9 @@ func TestCheckHealthTCPUDP(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "InvalidHost",
|
name: "InvalidHost",
|
||||||
scheme: "udp",
|
scheme: "udp",
|
||||||
host: "",
|
host: "invalid",
|
||||||
port: 8080,
|
port: 8080,
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusOK,
|
||||||
expectedHealthy: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Port in both host and port",
|
|
||||||
scheme: "tcp",
|
|
||||||
host: "localhost:1234",
|
|
||||||
port: 1234,
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
expectedHealthy: false,
|
expectedHealthy: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -216,11 +209,9 @@ func TestCheckHealthTCPUDP(t *testing.T) {
|
|||||||
|
|
||||||
require.Equal(t, recorder.Code, tt.expectedStatus)
|
require.Equal(t, recorder.Code, tt.expectedStatus)
|
||||||
|
|
||||||
if tt.expectedStatus == http.StatusOK {
|
var result health.HealthCheckResult
|
||||||
var result types.HealthCheckResult
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
||||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result))
|
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
||||||
require.Equal(t, result.Healthy, tt.expectedHealthy)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
agent/pkg/handler/docker_socket.go
Normal file
30
agent/pkg/handler/docker_socket.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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", &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: client.DummyHost,
|
||||||
|
}, dockerClient.HTTPClient().Transport)
|
||||||
|
|
||||||
|
return rp.ServeHTTP
|
||||||
|
}
|
||||||
@@ -2,59 +2,48 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"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/pkg"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
socketproxy "github.com/yusing/go-proxy/socketproxy/pkg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServeMux struct{ *http.ServeMux }
|
type ServeMux struct{ *http.ServeMux }
|
||||||
|
|
||||||
func (mux ServeMux) HandleEndpoint(method, endpoint string, handler http.HandlerFunc) {
|
func (mux ServeMux) HandleMethods(methods, endpoint string, handler http.HandlerFunc) {
|
||||||
mux.ServeMux.HandleFunc(method+" "+agent.APIEndpointBase+endpoint, handler)
|
for _, m := range strutils.CommaSeperatedList(methods) {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
var upgrader = &websocket.Upgrader{
|
type NopWriteCloser struct {
|
||||||
// no origin check needed for internal websocket
|
io.Writer
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
}
|
||||||
return true
|
|
||||||
},
|
func (NopWriteCloser) Close() error {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAgentHandler() http.Handler {
|
func NewAgentHandler() http.Handler {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
mux := ServeMux{http.NewServeMux()}
|
mux := ServeMux{http.NewServeMux()}
|
||||||
|
|
||||||
metricsHandler := gin.Default()
|
|
||||||
{
|
|
||||||
metrics := metricsHandler.Group(agent.APIEndpointBase)
|
|
||||||
metrics.GET(agent.EndpointSystemInfo, func(c *gin.Context) {
|
|
||||||
c.Set("upgrader", upgrader)
|
|
||||||
systeminfo.Poller.ServeHTTP(c)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
|
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
|
||||||
mux.HandleEndpoint("GET", agent.EndpointVersion, func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleMethods("GET", agent.EndpointVersion, v1.GetVersion)
|
||||||
fmt.Fprint(w, pkg.GetVersion())
|
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.HandleEndpoint("GET", agent.EndpointRuntime, func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleMethods("GET", agent.EndpointHealth, CheckHealth)
|
||||||
fmt.Fprint(w, env.Runtime)
|
mux.HandleMethods("GET", agent.EndpointLogs, memlogger.HandlerFunc())
|
||||||
})
|
mux.HandleMethods("GET", agent.EndpointSystemInfo, systeminfo.Poller.ServeHTTP)
|
||||||
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
mux.ServeMux.HandleFunc("/", DockerSocketHandler())
|
||||||
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
|
|
||||||
mux.ServeMux.HandleFunc("/", socketproxy.DockerSocketHandler(env.DockerSocket))
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,30 +3,22 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/url"
|
||||||
"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/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, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
|
isHTTPS := strutils.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
|
||||||
skipTLSVerify, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxySkipTLSVerify))
|
skipTLSVerify := strutils.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
|
||||||
@@ -42,9 +34,11 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
scheme = "https"
|
scheme = "https"
|
||||||
}
|
}
|
||||||
|
|
||||||
transport := NewTransport()
|
var transport *http.Transport
|
||||||
if skipTLSVerify {
|
if skipTLSVerify {
|
||||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
transport = gphttp.NewTransportWithTLSConfig(&tls.Config{InsecureSkipVerify: true})
|
||||||
|
} else {
|
||||||
|
transport = gphttp.NewTransport()
|
||||||
}
|
}
|
||||||
|
|
||||||
if responseHeaderTimeout > 0 {
|
if responseHeaderTimeout > 0 {
|
||||||
@@ -55,13 +49,14 @@ 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
|
||||||
|
r.URL.Scheme = scheme
|
||||||
|
|
||||||
rp := &httputil.ReverseProxy{
|
logging.Debug().Msgf("proxy http request: %s %s", r.Method, r.URL.String())
|
||||||
Director: func(r *http.Request) {
|
|
||||||
r.URL.Scheme = scheme
|
rp := reverseproxy.NewReverseProxy("agent", &url.URL{
|
||||||
r.URL.Host = host
|
Scheme: scheme,
|
||||||
},
|
Host: host,
|
||||||
Transport: transport,
|
}, transport)
|
||||||
}
|
|
||||||
rp.ServeHTTP(w, r)
|
rp.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/env"
|
"github.com/yusing/go-proxy/agent/pkg/env"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/handler"
|
"github.com/yusing/go-proxy/agent/pkg/handler"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/server"
|
"github.com/yusing/go-proxy/internal/net/gphttp/server"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
)
|
)
|
||||||
@@ -33,11 +33,12 @@ func StartAgentServer(parent task.Parent, opt Options) {
|
|||||||
tlsConfig.ClientAuth = tls.NoClientCert
|
tlsConfig.ClientAuth = tls.NoClientCert
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger := logging.GetLogger()
|
||||||
agentServer := &http.Server{
|
agentServer := &http.Server{
|
||||||
Addr: fmt.Sprintf(":%d", opt.Port),
|
Addr: fmt.Sprintf(":%d", opt.Port),
|
||||||
Handler: handler.NewAgentHandler(),
|
Handler: handler.NewAgentHandler(),
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Start(parent, agentServer, nil, &log.Logger)
|
server.Start(parent, agentServer, logger)
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 138 KiB |
132
cmd/main.go
132
cmd/main.go
@@ -1,14 +1,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
||||||
"github.com/yusing/go-proxy/internal/auth"
|
debugapi "github.com/yusing/go-proxy/internal/api/v1/debug"
|
||||||
|
"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"
|
||||||
@@ -16,50 +18,130 @@ 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/migrations"
|
||||||
"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 {
|
||||||
wg.Go(fn)
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
fn()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
initProfiling()
|
initProfiling()
|
||||||
|
if err := migrations.RunMigrations(); err != nil {
|
||||||
|
gperr.LogFatal("migration error", err)
|
||||||
|
}
|
||||||
|
args := pkg.GetArgs(common.MainServerCommandValidator{})
|
||||||
|
|
||||||
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
switch args.Command {
|
||||||
log.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
|
case common.CommandReload:
|
||||||
log.Trace().Msg("trace enabled")
|
if err := query.ReloadServer(); err != nil {
|
||||||
parallel(
|
gperr.LogFatal("server reload error", err)
|
||||||
dnsproviders.InitProviders,
|
}
|
||||||
homepage.InitIconListCache,
|
rawLogger.Println("ok")
|
||||||
systeminfo.Poller.Start,
|
return
|
||||||
middleware.LoadComposeFiles,
|
case common.CommandListIcons:
|
||||||
)
|
icons, err := homepage.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 common.APIJWTSecret == nil {
|
if args.Command == common.CommandStart {
|
||||||
log.Warn().Msg("API_JWT_SECRET is not set, using random key")
|
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
|
||||||
common.APIJWTSecret = common.RandomJWTKey()
|
logging.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
|
||||||
|
logging.Trace().Msg("trace enabled")
|
||||||
|
parallel(
|
||||||
|
homepage.InitIconListCache,
|
||||||
|
homepage.InitIconCache,
|
||||||
|
homepage.InitOverridesConfig,
|
||||||
|
systeminfo.Poller.Start,
|
||||||
|
)
|
||||||
|
|
||||||
|
if common.APIJWTSecret == nil {
|
||||||
|
logging.Warn().Msg("API_JWT_SECRET is not set, using random key")
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.Load()
|
middleware.LoadComposeFiles()
|
||||||
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{
|
||||||
Proxy: true,
|
Proxy: true,
|
||||||
})
|
})
|
||||||
if err := auth.Initialize(); err != nil {
|
if err := auth.Initialize(); err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
logging.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||||
}
|
}
|
||||||
// API Handler needs to start after auth is initialized.
|
// API Handler needs to start after auth is initialized.
|
||||||
cfg.StartServers(&config.StartServersOptions{
|
cfg.StartServers(&config.StartServersOptions{
|
||||||
@@ -69,13 +151,23 @@ func main() {
|
|||||||
uptime.Poller.Start()
|
uptime.Poller.Start()
|
||||||
config.WatchChanges()
|
config.WatchChanges()
|
||||||
|
|
||||||
|
debugapi.StartServer(cfg)
|
||||||
|
|
||||||
task.WaitExit(cfg.Value().TimeoutShutdown)
|
task.WaitExit(cfg.Value().TimeoutShutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareDirectory(dir string) {
|
func prepareDirectory(dir string) {
|
||||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
if err = os.MkdirAll(dir, 0o755); err != nil {
|
if err = os.MkdirAll(dir, 0o755); err != nil {
|
||||||
log.Fatal().Msgf("failed to create directory %s: %v", dir, err)
|
logging.Fatal().Msgf("failed to create directory %s: %v", dir, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printJSON(obj any) {
|
||||||
|
j, err := json.MarshalIndent(obj, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
logging.Fatal().Err(err).Send()
|
||||||
|
}
|
||||||
|
rawLogger.Print(string(j)) // raw output for convenience using "jq"
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,43 +3,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const mb = 1024 * 1024
|
|
||||||
|
|
||||||
func initProfiling() {
|
func initProfiling() {
|
||||||
debug.SetGCPercent(-1)
|
runtime.GOMAXPROCS(2)
|
||||||
debug.SetMemoryLimit(50 * mb)
|
debug.SetMemoryLimit(100 * 1024 * 1024)
|
||||||
debug.SetMaxStack(4 * mb)
|
debug.SetMaxStack(15 * 1024 * 1024)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.Info().Msgf("pprof server started at http://localhost:7777/debug/pprof/")
|
log.Println(http.ListenAndServe(":7777", nil))
|
||||||
log.Error().Err(http.ListenAndServe(":7777", nil)).Msg("pprof server failed")
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(time.Second * 10)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for range ticker.C {
|
|
||||||
var m runtime.MemStats
|
|
||||||
runtime.ReadMemStats(&m)
|
|
||||||
log.Info().Msgf("-----------------------------------------------------")
|
|
||||||
log.Info().Msgf("Timestamp: %s", time.Now().Format(time.RFC3339))
|
|
||||||
log.Info().Msgf(" Go Heap - In Use (Alloc/HeapAlloc): %s", strutils.FormatByteSize(m.Alloc))
|
|
||||||
log.Info().Msgf(" Go Heap - Reserved from OS (HeapSys): %s", strutils.FormatByteSize(m.HeapSys))
|
|
||||||
log.Info().Msgf(" Go Stacks - In Use (StackInuse): %s", strutils.FormatByteSize(m.StackInuse))
|
|
||||||
log.Info().Msgf(" Go Runtime - Other Sys (MSpanInuse, MCacheInuse, BuckHashSys, GCSys, OtherSys): %s", strutils.FormatByteSize(m.MSpanInuse+m.MCacheInuse+m.BuckHashSys+m.GCSys+m.OtherSys))
|
|
||||||
log.Info().Msgf(" Go Runtime - Total from OS (Sys): %s", strutils.FormatByteSize(m.Sys))
|
|
||||||
log.Info().Msgf(" Number of Goroutines: %d", runtime.NumGoroutine())
|
|
||||||
log.Info().Msgf(" Number of GCs: %d", m.NumGC)
|
|
||||||
log.Info().Msg("-----------------------------------------------------")
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,21 @@
|
|||||||
---
|
---
|
||||||
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:${TAG:-latest}
|
image: ghcr.io/yusing/godoxy-frontend: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
|
|
||||||
tmpfs:
|
|
||||||
- /app/.next/cache # next image caching
|
|
||||||
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_FRONTEND_ALIASES:-godoxy}
|
proxy.aliases: godoxy
|
||||||
proxy.#1.port: ${GODOXY_FRONTEND_PORT:-3000}
|
proxy.godoxy.port: ${GODOXY_FRONTEND_PORT:-3000}
|
||||||
# proxy.#1.middlewares.cidr_whitelist: |
|
# proxy.godoxy.middlewares.cidr_whitelist: |
|
||||||
# status: 403
|
# status: 403
|
||||||
# message: IP not allowed
|
# message: IP not allowed
|
||||||
# allow:
|
# allow:
|
||||||
@@ -51,27 +24,16 @@ 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:${TAG:-latest}
|
image: ghcr.io/yusing/godoxy:latest
|
||||||
container_name: godoxy-proxy
|
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:ro
|
- ./error_pages:/app/error_pages
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
|
||||||
# To use autocert, certs will be stored in "./certs".
|
# To use autocert, certs will be stored in "./certs".
|
||||||
|
|||||||
@@ -15,57 +15,23 @@
|
|||||||
# options:
|
# options:
|
||||||
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||||
|
|
||||||
# 3. other providers, see https://docs.godoxy.dev/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. set security headers
|
# 1. block non local IP connections
|
||||||
# 2. block non local IP connections
|
# 2. redirect HTTP to HTTPS
|
||||||
# 3. redirect HTTP to HTTPS
|
|
||||||
#
|
#
|
||||||
middlewares:
|
# middlewares:
|
||||||
- use: CloudflareRealIP
|
# - use: CIDRWhitelist
|
||||||
- use: ModifyResponse
|
# allow:
|
||||||
set_headers:
|
# - "127.0.0.1"
|
||||||
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
|
# - "10.0.0.0/8"
|
||||||
Access-Control-Allow-Headers: "*"
|
# - "172.16.0.0/12"
|
||||||
Access-Control-Allow-Origin: "*"
|
# - "192.168.0.0/16"
|
||||||
Access-Control-Max-Age: 180
|
# status: 403
|
||||||
Vary: "*"
|
# message: "Forbidden"
|
||||||
X-XSS-Protection: 1; mode=block
|
# - use: RedirectHTTP
|
||||||
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
|
|
||||||
# allow:
|
|
||||||
# - "127.0.0.1"
|
|
||||||
# - "10.0.0.0/8"
|
|
||||||
# - "172.16.0.0/12"
|
|
||||||
# - "192.168.0.0/16"
|
|
||||||
# status: 403
|
|
||||||
# message: "Forbidden"
|
|
||||||
# - use: RedirectHTTP
|
|
||||||
|
|
||||||
# below enables access log
|
# below enables access log
|
||||||
access_log:
|
access_log:
|
||||||
@@ -107,15 +73,7 @@ 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)
|
# Check https://github.com/yusing/godoxy/wiki/Certificates-and-domain-matching#domain-matching
|
||||||
#
|
|
||||||
# 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://docs.godoxy.dev/Certificates-and-domain-matching
|
|
||||||
# for explaination of `match_domains`
|
# for explaination of `match_domains`
|
||||||
#
|
#
|
||||||
# match_domains:
|
# match_domains:
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
# Stage 1: deps
|
|
||||||
FROM golang:1.25.0-alpine AS deps
|
|
||||||
HEALTHCHECK NONE
|
|
||||||
|
|
||||||
# package version does not matter
|
|
||||||
# trunk-ignore(hadolint/DL3018)
|
|
||||||
RUN apk add --no-cache tzdata make libcap-setcap
|
|
||||||
|
|
||||||
# Stage 3: Final image
|
|
||||||
FROM alpine:3.22
|
|
||||||
|
|
||||||
LABEL maintainer="yusing@6uo.me"
|
|
||||||
LABEL proxy.exclude=1
|
|
||||||
|
|
||||||
# copy timezone data
|
|
||||||
COPY --from=deps /usr/share/zoneinfo /usr/share/zoneinfo
|
|
||||||
|
|
||||||
# copy certs
|
|
||||||
COPY --from=deps /etc/ssl/certs /etc/ssl/certs
|
|
||||||
|
|
||||||
ARG TARGET
|
|
||||||
ENV TARGET=${TARGET}
|
|
||||||
|
|
||||||
ENV DOCKER_HOST=unix:///var/run/docker.sock
|
|
||||||
|
|
||||||
# copy binary
|
|
||||||
COPY bin/${TARGET} /app/run
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN chown -R 1000:1000 /app
|
|
||||||
|
|
||||||
CMD ["/app/run"]
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
services:
|
|
||||||
app:
|
|
||||||
image: godoxy-dev
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: dev.Dockerfile
|
|
||||||
args:
|
|
||||||
- TARGET=godoxy
|
|
||||||
container_name: godoxy-proxy-dev
|
|
||||||
restart: unless-stopped
|
|
||||||
env_file: dev.env
|
|
||||||
environment:
|
|
||||||
TZ: Asia/Hong_Kong
|
|
||||||
API_ADDR: 127.0.0.1:8999
|
|
||||||
API_USER: dev
|
|
||||||
API_PASSWORD: 1234
|
|
||||||
API_SKIP_ORIGIN_CHECK: true
|
|
||||||
API_JWT_TTL: 24h
|
|
||||||
DEBUG: true
|
|
||||||
API_SECRET: 1234567891234567
|
|
||||||
labels:
|
|
||||||
proxy.exclude: true
|
|
||||||
proxy.#1.healthcheck.disable: true
|
|
||||||
ipc: host
|
|
||||||
network_mode: host
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./dev-data/config:/app/config
|
|
||||||
- ./dev-data/certs:/app/certs
|
|
||||||
- ./dev-data/error_pages:/app/error_pages:ro
|
|
||||||
- ./dev-data/data:/app/data
|
|
||||||
- ./dev-data/logs:/app/logs
|
|
||||||
- ~/certs/myCA.pem:/etc/ssl/certs/ca.crt:ro
|
|
||||||
tinyauth:
|
|
||||||
image: ghcr.io/steveiliop56/tinyauth:v3
|
|
||||||
container_name: tinyauth
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- SECRET=12345678912345671234567891234567
|
|
||||||
- APP_URL=https://tinyauth.my.app
|
|
||||||
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
|
||||||
labels:
|
|
||||||
proxy.tinyauth.port: "3000"
|
|
||||||
309
go.mod
309
go.mod
@@ -1,254 +1,149 @@
|
|||||||
module github.com/yusing/go-proxy
|
module github.com/yusing/go-proxy
|
||||||
|
|
||||||
go 1.25.1
|
go 1.24.2
|
||||||
|
|
||||||
replace github.com/yusing/go-proxy/agent => ./agent
|
// misc
|
||||||
|
|
||||||
replace github.com/yusing/go-proxy/internal/dnsproviders => ./internal/dnsproviders
|
require (
|
||||||
|
github.com/bytedance/sonic v1.13.2 // faster json unmarshal (for marshal it's using custom implementation)
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||||
|
github.com/go-acme/lego/v4 v4.22.2 // acme client
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 // validator
|
||||||
|
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||||
|
github.com/goccy/go-yaml v1.17.1 // yaml parsing for different config files
|
||||||
|
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/puzpuzpuz/xsync/v3 v3.5.1 // lock free map for concurrent operations
|
||||||
|
golang.org/x/text v0.24.0 // string utilities
|
||||||
|
golang.org/x/time v0.11.0 // time utilities
|
||||||
|
)
|
||||||
|
|
||||||
replace github.com/yusing/go-proxy/internal/utils => ./internal/utils
|
// http
|
||||||
|
|
||||||
replace github.com/coreos/go-oidc/v3 => github.com/godoxy-app/go-oidc/v3 v3.0.0-20250816044348-0630187cb14b
|
require (
|
||||||
|
github.com/coder/websocket v1.8.13 // websocket for API and agent
|
||||||
|
github.com/quic-go/quic-go v0.50.1 // http3 server
|
||||||
|
golang.org/x/net v0.39.0 // HTTP header utilities
|
||||||
|
)
|
||||||
|
|
||||||
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d
|
// authentication
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/coreos/go-oidc/v3 v3.14.1 // oidc authentication
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 // jwt for default auth
|
||||||
|
golang.org/x/crypto v0.37.0 // encrypting password with bcrypt
|
||||||
|
golang.org/x/oauth2 v0.29.0 // oauth2 authentication
|
||||||
|
)
|
||||||
|
|
||||||
|
// favicon extraction
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
|
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
|
||||||
github.com/coreos/go-oidc/v3 v3.15.0 // oidc authentication
|
|
||||||
github.com/docker/docker v28.4.0+incompatible // docker daemon
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
|
||||||
github.com/go-acme/lego/v4 v4.26.0 // acme client
|
|
||||||
github.com/go-playground/validator/v10 v10.27.0 // validator
|
|
||||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
|
||||||
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
|
||||||
github.com/gotify/server/v2 v2.7.2 // reference the Message struct for json response
|
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.1.0 // lock free map for concurrent operations
|
|
||||||
github.com/rs/zerolog v1.34.0 // logging
|
|
||||||
github.com/shirou/gopsutil/v4 v4.25.8 // 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.42.0 // encrypting password with bcrypt
|
|
||||||
golang.org/x/net v0.44.0 // HTTP header utilities
|
|
||||||
golang.org/x/oauth2 v0.31.0 // oauth2 authentication
|
|
||||||
golang.org/x/sync v0.17.0
|
|
||||||
golang.org/x/time v0.13.0 // time utilities
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
// docker
|
||||||
github.com/docker/cli v28.4.0+incompatible
|
|
||||||
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
|
require (
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/docker/cli v28.0.4+incompatible // docker cli
|
||||||
github.com/luthermonson/go-proxmox v0.2.3
|
github.com/docker/docker v28.0.4+incompatible // docker daemon
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1
|
github.com/docker/go-connections v0.5.0 // docker connection utilities
|
||||||
github.com/quic-go/quic-go v0.54.0
|
)
|
||||||
github.com/samber/slog-zerolog/v2 v2.7.3
|
|
||||||
github.com/spf13/afero v1.15.0
|
// logging
|
||||||
github.com/stretchr/testify v1.11.1
|
|
||||||
github.com/yusing/go-proxy/agent v0.0.0-20250913143824-493c0afdface
|
require (
|
||||||
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-20250913143824-493c0afdface
|
github.com/rs/zerolog v1.34.0 // logging
|
||||||
github.com/yusing/go-proxy/internal/utils v0.0.0
|
github.com/samber/slog-zerolog/v2 v2.7.3 // zerlog to slog adapter for quic-go
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// metrics
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/prometheus/client_golang v1.22.0 // metrics
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.3 // system info metrics
|
||||||
|
github.com/stretchr/testify v1.10.0 // testing utilities
|
||||||
|
)
|
||||||
|
|
||||||
|
require github.com/luthermonson/go-proxmox v0.2.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/auth v0.16.5 // indirect
|
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
|
||||||
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
|
||||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 // indirect
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // 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.5.0 // 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/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.39.0 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.8 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // 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.13.1 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.4 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.2 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 // indirect
|
|
||||||
github.com/aws/smithy-go v1.23.0 // indirect
|
|
||||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
|
||||||
github.com/boombuler/barcode v1.1.0 // indirect
|
|
||||||
github.com/buger/goterm v1.0.4 // indirect
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.5 // 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.7.0 // indirect
|
github.com/diskfs/go-diskfs v1.6.0 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/djherbis/times v1.6.0 // indirect
|
github.com/djherbis/times v1.6.0 // indirect
|
||||||
github.com/docker/go-connections v0.6.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.4 // indirect
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
github.com/exoscale/egoscale/v3 v3.1.26 // 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.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/go-errors/errors v1.5.1 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/gofrs/flock v0.12.1 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
|
||||||
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
|
||||||
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
|
||||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
|
||||||
github.com/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/jinzhu/copier v0.4.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // 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.57.0 // indirect
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // 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-20250827001030-24949be3fa54 // indirect
|
|
||||||
github.com/magefile/mage v1.15.0 // indirect
|
github.com/magefile/mage v1.15.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/miekg/dns v1.1.68 // indirect
|
github.com/miekg/dns v1.1.65 // indirect
|
||||||
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/moby/term v0.5.0 // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/nrdcg/auroradns v1.1.0 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/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/ovh/go-ovh v1.9.0 // indirect
|
github.com/ovh/go-ovh v1.7.0 // 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/pquerna/otp v1.5.0 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.63.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.16.0 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
|
github.com/samber/lo v1.49.1 // indirect
|
||||||
github.com/sacloud/api-client-go v0.3.3 // indirect
|
github.com/samber/slog-common v0.18.1 // indirect
|
||||||
github.com/sacloud/go-http v0.1.9 // indirect
|
|
||||||
github.com/sacloud/iaas-api-go v1.17.1 // indirect
|
|
||||||
github.com/sacloud/packages-go v0.0.11 // indirect
|
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
|
||||||
github.com/samber/lo v1.51.0 // indirect
|
|
||||||
github.com/samber/slog-common v0.19.0 // indirect
|
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 // indirect
|
|
||||||
github.com/selectel/domains-go v1.1.0 // indirect
|
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
|
||||||
github.com/softlayer/softlayer-go v1.2.1 // 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.1-0.20240121214520-5f936abd7ae8 // indirect
|
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
|
||||||
github.com/spf13/viper v1.21.0 // indirect
|
|
||||||
github.com/subosito/gotenv v1.6.0 // 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.1-20250722213956-faef419 // indirect
|
|
||||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
|
||||||
github.com/volcengine/volc-sdk-golang v1.0.219 // indirect
|
|
||||||
github.com/vultr/govultr/v3 v3.23.0 // indirect
|
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
|
||||||
go.opentelemetry.io/auto/sdk v1.2.0 // indirect
|
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
|
||||||
go.uber.org/atomic v1.11.0
|
|
||||||
go.uber.org/mock v0.6.0 // indirect
|
|
||||||
go.uber.org/ratelimit v0.3.1 // indirect
|
|
||||||
golang.org/x/mod v0.28.0 // indirect
|
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
|
||||||
golang.org/x/text v0.29.0 // indirect
|
|
||||||
golang.org/x/tools v0.37.0 // indirect
|
|
||||||
google.golang.org/api v0.249.0 // indirect
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
|
|
||||||
google.golang.org/grpc v1.75.1 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
|
||||||
gopkg.in/ns1/ns1-go.v2 v2.15.0 // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/gin-gonic/gin v1.10.1
|
|
||||||
github.com/yusing/ds v0.1.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
|
||||||
github.com/aziontech/azionapi-go-sdk v0.142.0 // indirect
|
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
|
||||||
github.com/bytedance/sonic v1.14.1 // indirect
|
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
|
||||||
github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
|
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
|
||||||
github.com/moby/term v0.5.2 // indirect
|
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
|
||||||
github.com/namedotcom/go/v4 v4.0.2 // indirect
|
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.100.0 // indirect
|
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.100.0 // indirect
|
|
||||||
github.com/onsi/ginkgo/v2 v2.25.1 // indirect
|
|
||||||
github.com/onsi/gomega v1.38.1 // indirect
|
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
|
||||||
github.com/selectel/go-selvpcclient/v4 v4.1.0 // indirect
|
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||||
golang.org/x/arch v0.21.0 // indirect
|
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect
|
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||||
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.1 // indirect
|
||||||
|
golang.org/x/arch v0.16.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
|
golang.org/x/tools v0.32.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gotest.tools/v3 v3.5.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/puzpuzpuz/xsync/v4"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
"github.com/yusing/go-proxy/internal/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
|
|
||||||
}
|
|
||||||
log.Info().
|
|
||||||
Str("default", c.Default).
|
|
||||||
Bool("allow_local", c.allowLocal).
|
|
||||||
Int("allow_rules", len(c.Allow)).
|
|
||||||
Int("deny_rules", len(c.Deny)).
|
|
||||||
Msg("ACL started")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
maxmind "github.com/yusing/go-proxy/internal/maxmind/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/serialization"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMatchers(t *testing.T) {
|
|
||||||
strMatchers := []string{
|
|
||||||
"ip:127.0.0.1",
|
|
||||||
"cidr:10.0.0.0/8",
|
|
||||||
}
|
|
||||||
|
|
||||||
var mathers Matchers
|
|
||||||
err := serialization.Convert(reflect.ValueOf(strMatchers), reflect.ValueOf(&mathers), false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
ip string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"127.0.0.1", true},
|
|
||||||
{"10.0.0.1", true},
|
|
||||||
{"127.0.0.2", false},
|
|
||||||
{"192.168.0.1", false},
|
|
||||||
{"11.0.0.1", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
ip := net.ParseIP(test.ip)
|
|
||||||
if ip == nil {
|
|
||||||
t.Fatalf("invalid ip: %s", test.ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := mathers.Match(&maxmind.IPInfo{
|
|
||||||
IP: ip,
|
|
||||||
Str: test.ip,
|
|
||||||
})
|
|
||||||
if got != test.want {
|
|
||||||
t.Errorf("mathers.Match(%s) = %v, want %v", test.ip, got, test.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
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 (c *Config) WrapTCP(lis net.Listener) net.Listener {
|
|
||||||
if c == nil {
|
|
||||||
return lis
|
|
||||||
}
|
|
||||||
return &TCPListener{
|
|
||||||
acl: c,
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -2,218 +2,69 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"github.com/gorilla/websocket"
|
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
"github.com/yusing/go-proxy/internal/api/v1/certapi"
|
||||||
apiV1 "github.com/yusing/go-proxy/internal/api/v1"
|
"github.com/yusing/go-proxy/internal/api/v1/dockerapi"
|
||||||
agentApi "github.com/yusing/go-proxy/internal/api/v1/agent"
|
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
||||||
authApi "github.com/yusing/go-proxy/internal/api/v1/auth"
|
|
||||||
certApi "github.com/yusing/go-proxy/internal/api/v1/cert"
|
|
||||||
dockerApi "github.com/yusing/go-proxy/internal/api/v1/docker"
|
|
||||||
fileApi "github.com/yusing/go-proxy/internal/api/v1/file"
|
|
||||||
homepageApi "github.com/yusing/go-proxy/internal/api/v1/homepage"
|
|
||||||
metricsApi "github.com/yusing/go-proxy/internal/api/v1/metrics"
|
|
||||||
routeApi "github.com/yusing/go-proxy/internal/api/v1/route"
|
|
||||||
"github.com/yusing/go-proxy/internal/auth"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
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/metrics/uptime"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp/servemux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @title GoDoxy API
|
func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||||
// @version 1.0
|
mux := servemux.NewServeMux(cfg)
|
||||||
// @description GoDoxy API
|
mux.HandleFunc("GET", "/v1", v1.Index)
|
||||||
// @termsOfService https://github.com/yusing/godoxy/blob/main/LICENSE
|
mux.HandleFunc("GET", "/v1/version", v1.GetVersion)
|
||||||
|
|
||||||
// @contact.name Yusing
|
mux.HandleFunc("GET", "/v1/stats", v1.Stats, true)
|
||||||
// @contact.url https://github.com/yusing/godoxy/issues
|
mux.HandleFunc("POST", "/v1/reload", v1.Reload, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/list", v1.List, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/list/{what}", v1.List, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/list/{what}/{which}", v1.List, 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", "/v1/file/validate/{type}", v1.ValidateFile, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/health", v1.Health, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/logs", memlogger.Handler(), true)
|
||||||
|
mux.HandleFunc("GET", "/v1/favicon", favicon.GetFavIcon, true)
|
||||||
|
mux.HandleFunc("POST", "/v1/homepage/set", v1.SetHomePageOverrides, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/agents", v1.ListAgents, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/agents/new", v1.NewAgent, true)
|
||||||
|
mux.HandleFunc("POST", "/v1/agents/verify", v1.VerifyNewAgent, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/metrics/system_info", v1.SystemInfo, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/metrics/uptime", uptime.Poller.ServeHTTP, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/cert/info", certapi.GetCertInfo, true)
|
||||||
|
mux.HandleFunc("", "/v1/cert/renew", certapi.RenewCert, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/docker/info", dockerapi.DockerInfo, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/docker/logs/{server}/{container}", dockerapi.Logs, true)
|
||||||
|
mux.HandleFunc("GET", "/v1/docker/containers", dockerapi.Containers, true)
|
||||||
|
|
||||||
// @license.name MIT
|
if common.PrometheusEnabled {
|
||||||
// @license.url https://github.com/yusing/godoxy/blob/main/LICENSE
|
mux.Handle("GET /v1/metrics", promhttp.Handler())
|
||||||
|
logging.Info().Msg("prometheus metrics enabled")
|
||||||
// @BasePath /api/v1
|
|
||||||
|
|
||||||
// @externalDocs.description GoDoxy Docs
|
|
||||||
// @externalDocs.url https://docs.godoxy.dev
|
|
||||||
func NewHandler() *gin.Engine {
|
|
||||||
if !common.IsDebug {
|
|
||||||
gin.SetMode("release")
|
|
||||||
}
|
|
||||||
r := gin.New()
|
|
||||||
r.Use(ErrorHandler())
|
|
||||||
r.Use(ErrorLoggingMiddleware())
|
|
||||||
|
|
||||||
r.GET("/api/v1/version", apiV1.Version)
|
|
||||||
|
|
||||||
if auth.IsEnabled() {
|
|
||||||
v1Auth := r.Group("/api/v1/auth")
|
|
||||||
{
|
|
||||||
v1Auth.HEAD("/check", authApi.Check)
|
|
||||||
v1Auth.POST("/login", authApi.Login)
|
|
||||||
v1Auth.GET("/callback", authApi.Callback)
|
|
||||||
v1Auth.POST("/callback", authApi.Callback)
|
|
||||||
v1Auth.POST("/logout", authApi.Logout)
|
|
||||||
v1Auth.GET("/logout", authApi.Logout)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
v1 := r.Group("/api/v1")
|
defaultAuth := auth.GetDefaultAuth()
|
||||||
if auth.IsEnabled() {
|
if defaultAuth != nil {
|
||||||
v1.Use(AuthMiddleware())
|
mux.HandleFunc("GET", "/v1/auth/redirect", defaultAuth.RedirectLoginPage)
|
||||||
}
|
mux.HandleFunc("GET", "/v1/auth/check", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if common.APISkipOriginCheck {
|
if err := defaultAuth.CheckToken(r); err != nil {
|
||||||
v1.Use(SkipOriginCheckMiddleware())
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
}
|
return
|
||||||
{
|
|
||||||
// enable cache for favicon
|
|
||||||
v1.GET("/favicon", apiV1.FavIcon).Use(Cache(time.Hour * 24))
|
|
||||||
v1.GET("/health", apiV1.Health)
|
|
||||||
v1.GET("/icons", apiV1.Icons)
|
|
||||||
v1.POST("/reload", apiV1.Reload)
|
|
||||||
v1.GET("/stats", apiV1.Stats)
|
|
||||||
|
|
||||||
route := v1.Group("/route")
|
|
||||||
{
|
|
||||||
route.GET("/list", routeApi.Routes)
|
|
||||||
route.GET("/:which", routeApi.Route)
|
|
||||||
route.GET("/providers", routeApi.Providers)
|
|
||||||
route.GET("/by_provider", routeApi.ByProvider)
|
|
||||||
}
|
|
||||||
|
|
||||||
file := v1.Group("/file")
|
|
||||||
{
|
|
||||||
file.GET("/list", fileApi.List)
|
|
||||||
file.GET("/content", fileApi.Get)
|
|
||||||
file.PUT("/content", fileApi.Set)
|
|
||||||
file.POST("/content", fileApi.Set)
|
|
||||||
file.POST("/validate", fileApi.Validate)
|
|
||||||
}
|
|
||||||
|
|
||||||
homepage := v1.Group("/homepage")
|
|
||||||
{
|
|
||||||
homepage.GET("/categories", homepageApi.Categories)
|
|
||||||
homepage.GET("/items", homepageApi.Items)
|
|
||||||
homepage.POST("/set/item", homepageApi.SetItem)
|
|
||||||
homepage.POST("/set/items_batch", homepageApi.SetItemsBatch)
|
|
||||||
homepage.POST("/set/item_visible", homepageApi.SetItemVisible)
|
|
||||||
homepage.POST("/set/item_favorite", homepageApi.SetItemFavorite)
|
|
||||||
homepage.POST("/set/item_sort_order", homepageApi.SetItemSortOrder)
|
|
||||||
homepage.POST("/set/item_all_sort_order", homepageApi.SetItemAllSortOrder)
|
|
||||||
homepage.POST("/set/item_fav_sort_order", homepageApi.SetItemFavSortOrder)
|
|
||||||
homepage.POST("/set/category_order", homepageApi.SetCategoryOrder)
|
|
||||||
homepage.POST("/item_click", homepageApi.ItemClick)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert := v1.Group("/cert")
|
|
||||||
{
|
|
||||||
cert.GET("/info", certApi.Info)
|
|
||||||
cert.GET("/renew", certApi.Renew)
|
|
||||||
}
|
|
||||||
|
|
||||||
agent := v1.Group("/agent")
|
|
||||||
{
|
|
||||||
agent.GET("/list", agentApi.List)
|
|
||||||
agent.POST("/create", agentApi.Create)
|
|
||||||
agent.POST("/verify", agentApi.Verify)
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics := v1.Group("/metrics")
|
|
||||||
{
|
|
||||||
metrics.GET("/system_info", metricsApi.SystemInfo)
|
|
||||||
metrics.GET("/all_system_info", metricsApi.AllSystemInfo)
|
|
||||||
metrics.GET("/uptime", metricsApi.Uptime)
|
|
||||||
}
|
|
||||||
|
|
||||||
docker := v1.Group("/docker")
|
|
||||||
{
|
|
||||||
docker.GET("/container/:id", dockerApi.GetContainer)
|
|
||||||
docker.GET("/containers", dockerApi.Containers)
|
|
||||||
docker.GET("/info", dockerApi.Info)
|
|
||||||
docker.GET("/logs/:id", dockerApi.Logs)
|
|
||||||
docker.POST("/start", dockerApi.Start)
|
|
||||||
docker.POST("/stop", dockerApi.Stop)
|
|
||||||
docker.POST("/restart", dockerApi.Restart)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// disable cache by default
|
|
||||||
r.Use(NoCache())
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func NoCache() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
// skip cache if Cache-Control header is set or if caching is explicitly enabled
|
|
||||||
if !c.GetBool("cache_enabled") && c.Writer.Header().Get("Cache-Control") == "" {
|
|
||||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
||||||
c.Header("Pragma", "no-cache")
|
|
||||||
c.Header("Expires", "0")
|
|
||||||
}
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Cache(duration time.Duration) gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
// Signal to NoCache middleware that caching is intended
|
|
||||||
c.Set("cache_enabled", true)
|
|
||||||
// skip cache if Cache-Control header is set
|
|
||||||
if c.Writer.Header().Get("Cache-Control") == "" {
|
|
||||||
c.Header("Cache-Control", "public, max-age="+strconv.FormatFloat(duration.Seconds(), 'f', 0, 64)+", immutable")
|
|
||||||
c.Header("Pragma", "public")
|
|
||||||
c.Header("Expires", time.Now().Add(duration).Format(time.RFC1123))
|
|
||||||
}
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AuthMiddleware() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
err := auth.GetDefaultAuth().CheckToken(c.Request)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusUnauthorized, apitypes.Error("Unauthorized", err))
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SkipOriginCheckMiddleware() gin.HandlerFunc {
|
|
||||||
upgrader := &websocket.Upgrader{
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
c.Set("upgrader", upgrader)
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrorHandler() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
c.Next()
|
|
||||||
if len(c.Errors) > 0 {
|
|
||||||
logger := log.With().Str("uri", c.Request.RequestURI).Logger()
|
|
||||||
for _, err := range c.Errors {
|
|
||||||
gperr.LogError("Internal error", err.Err, &logger)
|
|
||||||
}
|
}
|
||||||
if !c.IsWebsocket() {
|
})
|
||||||
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
|
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
|
||||||
|
|
||||||
func ErrorLoggingMiddleware() gin.HandlerFunc {
|
|
||||||
return gin.CustomRecoveryWithWriter(nil, func(c *gin.Context, err any) {
|
|
||||||
log.Error().Any("error", err).Str("uri", c.Request.RequestURI).Msg("Internal error")
|
|
||||||
if !c.IsWebsocket() {
|
|
||||||
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
package apitypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ErrorResponse struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Error string `json:"error,omitempty" extensions:"x-nullable"`
|
|
||||||
} // @name ErrorResponse
|
|
||||||
|
|
||||||
type serverError struct {
|
|
||||||
Message string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns a generic error response
|
|
||||||
func Error(message string, err ...error) ErrorResponse {
|
|
||||||
if len(err) > 0 {
|
|
||||||
var gpErr gperr.Error
|
|
||||||
if errors.As(err[0], &gpErr) {
|
|
||||||
return ErrorResponse{
|
|
||||||
Message: message,
|
|
||||||
Error: string(gpErr.Plain()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ErrorResponse{
|
|
||||||
Message: message,
|
|
||||||
Error: err[0].Error(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ErrorResponse{
|
|
||||||
Message: message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func InternalServerError(err error, message string) error {
|
|
||||||
return serverError{
|
|
||||||
Message: message,
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e serverError) Error() string {
|
|
||||||
if e.Err != nil {
|
|
||||||
return e.Message + ": " + e.Err.Error()
|
|
||||||
}
|
|
||||||
return e.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e serverError) Unwrap() error {
|
|
||||||
return e.Err
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package apitypes
|
|
||||||
|
|
||||||
type QueryOptions struct {
|
|
||||||
Limit int `binding:"required,min=1,max=20" form:"limit"`
|
|
||||||
Offset int `binding:"omitempty,min=0" form:"offset"`
|
|
||||||
OrderBy QueryOrder `binding:"omitempty,oneof=created_at updated_at" form:"order_by"`
|
|
||||||
Order QueryOrderDirection `binding:"omitempty,oneof=asc desc" form:"order"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type QueryOrder string
|
|
||||||
|
|
||||||
const (
|
|
||||||
QueryOrderCreatedAt QueryOrder = "created_at"
|
|
||||||
QueryOrderUpdatedAt QueryOrder = "updated_at"
|
|
||||||
)
|
|
||||||
|
|
||||||
type QueryOrderDirection string
|
|
||||||
|
|
||||||
const (
|
|
||||||
QueryOrderDirectionAsc QueryOrderDirection = "asc"
|
|
||||||
QueryOrderDirectionDesc QueryOrderDirection = "desc"
|
|
||||||
)
|
|
||||||
|
|
||||||
type QueryResponse struct {
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
Offset int `json:"offset"`
|
|
||||||
HasMore bool `json:"has_more"`
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package apitypes
|
|
||||||
|
|
||||||
type SuccessResponse struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Details map[string]any `json:"details,omitempty" extensions:"x-nullable"`
|
|
||||||
} // @name SuccessResponse
|
|
||||||
|
|
||||||
func Success(message string, extra ...map[string]any) SuccessResponse {
|
|
||||||
if len(extra) > 0 {
|
|
||||||
return SuccessResponse{
|
|
||||||
Message: message,
|
|
||||||
Details: extra[0],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return SuccessResponse{
|
|
||||||
Message: message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package agentapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PEMPairResponse struct {
|
|
||||||
Cert string `json:"cert" format:"base64"`
|
|
||||||
Key string `json:"key" format:"base64"`
|
|
||||||
} // @name PEMPairResponse
|
|
||||||
|
|
||||||
var encryptionKey atomic.Value
|
|
||||||
|
|
||||||
const rotateKeyInterval = 15 * time.Minute
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if err := rotateKey(); err != nil {
|
|
||||||
log.Panic().Err(err).Msg("failed to generate encryption key")
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
for range time.Tick(rotateKeyInterval) {
|
|
||||||
if err := rotateKey(); err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to rotate encryption key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getEncryptionKey() []byte {
|
|
||||||
return encryptionKey.Load().([]byte)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rotateKey() error {
|
|
||||||
// generate a random 32 bytes key
|
|
||||||
key := make([]byte, 32)
|
|
||||||
if _, err := rand.Read(key); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
encryptionKey.Store(key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func toPEMPairResponse(encPEMPair agent.PEMPair) PEMPairResponse {
|
|
||||||
return PEMPairResponse{
|
|
||||||
Cert: base64.StdEncoding.EncodeToString(encPEMPair.Cert),
|
|
||||||
Key: base64.StdEncoding.EncodeToString(encPEMPair.Key),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fromEncryptedPEMPairResponse(pemPair PEMPairResponse) (agent.PEMPair, error) {
|
|
||||||
encCert, err := base64.StdEncoding.DecodeString(pemPair.Cert)
|
|
||||||
if err != nil {
|
|
||||||
return agent.PEMPair{}, err
|
|
||||||
}
|
|
||||||
encKey, err := base64.StdEncoding.DecodeString(pemPair.Key)
|
|
||||||
if err != nil {
|
|
||||||
return agent.PEMPair{}, err
|
|
||||||
}
|
|
||||||
pair := agent.PEMPair{Cert: encCert, Key: encKey}
|
|
||||||
return pair.Decrypt(getEncryptionKey())
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
package agentapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
_ "embed"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NewAgentRequest struct {
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Host string `json:"host" binding:"required"`
|
|
||||||
Port int `json:"port" binding:"required,min=1,max=65535"`
|
|
||||||
Type string `json:"type" binding:"required,oneof=docker system"`
|
|
||||||
Nightly bool `json:"nightly" binding:"omitempty"`
|
|
||||||
ContainerRuntime agent.ContainerRuntime `json:"container_runtime" binding:"omitempty,oneof=docker podman" default:"docker"`
|
|
||||||
} // @name NewAgentRequest
|
|
||||||
|
|
||||||
type NewAgentResponse struct {
|
|
||||||
Compose string `json:"compose"`
|
|
||||||
CA PEMPairResponse `json:"ca"`
|
|
||||||
Client PEMPairResponse `json:"client"`
|
|
||||||
} // @name NewAgentResponse
|
|
||||||
|
|
||||||
// @x-id "create"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Create a new agent
|
|
||||||
// @Description Create a new agent and return the docker compose file, encrypted CA and client PEMs
|
|
||||||
// @Description The returned PEMs are encrypted with a random key and will be used for verification when adding a new agent
|
|
||||||
// @Tags agent
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body NewAgentRequest true "Request"
|
|
||||||
// @Success 200 {object} NewAgentResponse
|
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 409 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /agent/create [post]
|
|
||||||
func Create(c *gin.Context) {
|
|
||||||
var request NewAgentRequest
|
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hostport := net.JoinHostPort(request.Host, strconv.Itoa(request.Port))
|
|
||||||
if _, ok := agent.GetAgent(hostport); ok {
|
|
||||||
c.JSON(http.StatusConflict, apitypes.Error("agent already exists"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var image string
|
|
||||||
if request.Nightly {
|
|
||||||
image = agent.DockerImageNightly
|
|
||||||
} else {
|
|
||||||
image = agent.DockerImageProduction
|
|
||||||
}
|
|
||||||
|
|
||||||
ca, srv, client, err := agent.NewAgent()
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to create agent"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var cfg agent.Generator = &agent.AgentEnvConfig{
|
|
||||||
Name: request.Name,
|
|
||||||
Port: request.Port,
|
|
||||||
CACert: ca.String(),
|
|
||||||
SSLCert: srv.String(),
|
|
||||||
ContainerRuntime: request.ContainerRuntime,
|
|
||||||
}
|
|
||||||
if request.Type == "docker" {
|
|
||||||
cfg = &agent.AgentComposeConfig{
|
|
||||||
Image: image,
|
|
||||||
AgentEnvConfig: cfg.(*agent.AgentEnvConfig),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
template, err := cfg.Generate()
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to generate agent config"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
key := getEncryptionKey()
|
|
||||||
encCA, err := ca.Encrypt(key)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to encrypt CA PEMs"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
encClient, err := client.Encrypt(key)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to encrypt client PEMs"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, NewAgentResponse{
|
|
||||||
Compose: template,
|
|
||||||
CA: toPEMPairResponse(encCA),
|
|
||||||
Client: toPEMPairResponse(encClient),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package agentapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @x-id "list"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary List agents
|
|
||||||
// @Description List agents
|
|
||||||
// @Tags agent,websocket
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {array} Agent
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /agent/list [get]
|
|
||||||
func List(c *gin.Context) {
|
|
||||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
|
||||||
websocket.PeriodicWrite(c, 10*time.Second, func() (any, error) {
|
|
||||||
return agent.ListAgents(), nil
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
c.JSON(http.StatusOK, agent.ListAgents())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package agentapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
|
||||||
"github.com/yusing/go-proxy/agent/pkg/certs"
|
|
||||||
. "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type VerifyNewAgentRequest struct {
|
|
||||||
Host string `json:"host"`
|
|
||||||
CA PEMPairResponse `json:"ca"`
|
|
||||||
Client PEMPairResponse `json:"client"`
|
|
||||||
ContainerRuntime agent.ContainerRuntime `json:"container_runtime"`
|
|
||||||
} // @name VerifyNewAgentRequest
|
|
||||||
|
|
||||||
// @x-id "verify"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Verify a new agent
|
|
||||||
// @Description Verify a new agent and return the number of routes added
|
|
||||||
// @Tags agent
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body VerifyNewAgentRequest true "Request"
|
|
||||||
// @Success 200 {object} SuccessResponse
|
|
||||||
// @Failure 400 {object} ErrorResponse
|
|
||||||
// @Failure 403 {object} ErrorResponse
|
|
||||||
// @Failure 500 {object} ErrorResponse
|
|
||||||
// @Router /agent/verify [post]
|
|
||||||
func Verify(c *gin.Context) {
|
|
||||||
var request VerifyNewAgentRequest
|
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, Error("invalid request", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
filename, ok := certs.AgentCertsFilepath(request.Host)
|
|
||||||
if !ok {
|
|
||||||
c.JSON(http.StatusBadRequest, Error("invalid host", nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ca, err := fromEncryptedPEMPairResponse(request.CA)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, Error("invalid CA", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := fromEncryptedPEMPairResponse(request.Client)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, Error("invalid client", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, Error("invalid request", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
zip, err := certs.ZipCert(ca.Cert, client.Cert, client.Key)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(InternalServerError(err, "failed to zip certs"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(filename, zip, 0o600); err != nil {
|
|
||||||
c.Error(InternalServerError(err, "failed to write certs"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
|
|
||||||
}
|
|
||||||
14
internal/api/v1/agents.go
Normal file
14
internal/api/v1/agents.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListAgents(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
gpwebsocket.DynamicJSONHandler(w, r, agent.Agents.Slice, 10*time.Second)
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultAuth Provider
|
var defaultAuth Provider
|
||||||
@@ -37,24 +38,15 @@ func IsOIDCEnabled() bool {
|
|||||||
return common.OIDCIssuerURL != ""
|
return common.OIDCIssuerURL != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
type nextHandler struct{}
|
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
if IsEnabled() {
|
||||||
var nextHandlerContextKey = nextHandler{}
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := defaultAuth.CheckToken(r); err != nil {
|
||||||
func ProceedNext(w http.ResponseWriter, r *http.Request) {
|
gphttp.ClientError(w, err, http.StatusUnauthorized)
|
||||||
next, ok := r.Context().Value(nextHandlerContextKey).(http.HandlerFunc)
|
} else {
|
||||||
if ok {
|
next(w, r)
|
||||||
next(w, r)
|
}
|
||||||
} else {
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := defaultAuth.CheckToken(r)
|
|
||||||
if err != nil {
|
|
||||||
defaultAuth.LoginHandler(w, r)
|
|
||||||
} else {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
}
|
||||||
|
return next
|
||||||
}
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
//nolint:dupword
|
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/yusing/go-proxy/internal/auth"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @x-id "callback"
|
|
||||||
// @Base /api/v1
|
|
||||||
// @Summary Auth Callback
|
|
||||||
// @Description Handles the callback from the provider after successful authentication
|
|
||||||
// @Tags auth
|
|
||||||
// @Produce plain
|
|
||||||
// @Param body body auth.UserPassAuthCallbackRequest true "Userpass only"
|
|
||||||
// @Success 200 {string} string "Userpass: OK"
|
|
||||||
// @Success 302 {string} string "OIDC: Redirects to home page"
|
|
||||||
// @Failure 400 {string} string "OIDC: invalid request (missing state cookie or oauth state)"
|
|
||||||
// @Failure 400 {string} string "Userpass: invalid request / credentials"
|
|
||||||
// @Failure 500 {string} string "Internal server error"
|
|
||||||
// @Router /auth/callback [post]
|
|
||||||
func Callback(c *gin.Context) {
|
|
||||||
auth.GetDefaultAuth().PostAuthCallbackHandler(c.Writer, c.Request)
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/yusing/go-proxy/internal/auth"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @x-id "check"
|
|
||||||
// @Base /api/v1
|
|
||||||
// @Summary Check authentication status
|
|
||||||
// @Description Checks if the user is authenticated by validating their token
|
|
||||||
// @Tags auth
|
|
||||||
// @Produce plain
|
|
||||||
// @Success 200 {string} string "OK"
|
|
||||||
// @Failure 302 {string} string "Redirects to login page or IdP"
|
|
||||||
// @Router /auth/check [head]
|
|
||||||
func Check(c *gin.Context) {
|
|
||||||
auth.AuthCheckHandler(c.Writer, c.Request)
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/yusing/go-proxy/internal/auth"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @x-id "login"
|
|
||||||
// @Base /api/v1
|
|
||||||
// @Summary Login
|
|
||||||
// @Description Initiates the login process by redirecting the user to the provider's login page
|
|
||||||
// @Tags auth
|
|
||||||
// @Produce plain
|
|
||||||
// @Success 302 {string} string "Redirects to login page or IdP"
|
|
||||||
// @Failure 429 {string} string "Too Many Requests"
|
|
||||||
// @Router /auth/login [post]
|
|
||||||
func Login(c *gin.Context) {
|
|
||||||
auth.GetDefaultAuth().LoginHandler(c.Writer, c.Request)
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/yusing/go-proxy/internal/auth"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @x-id "logout"
|
|
||||||
// @Base /api/v1
|
|
||||||
// @Summary Logout
|
|
||||||
// @Description Logs out the user by invalidating the token
|
|
||||||
// @Tags auth
|
|
||||||
// @Produce plain
|
|
||||||
// @Success 302 {string} string "Redirects to home page"
|
|
||||||
// @Router /auth/logout [post]
|
|
||||||
// @Router /auth/logout [get]
|
|
||||||
func Logout(c *gin.Context) {
|
|
||||||
auth.GetDefaultAuth().LogoutHandler(c.Writer, c.Request)
|
|
||||||
}
|
|
||||||
309
internal/api/v1/auth/oidc.go
Normal file
309
internal/api/v1/auth/oidc.go
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/pkg/json"
|
||||||
|
|
||||||
|
"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,16 +1,17 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/pkg/json"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
@@ -23,7 +24,7 @@ import (
|
|||||||
func setupMockOIDC(t *testing.T) {
|
func setupMockOIDC(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
provider := (&oidc.ProviderConfig{}).NewProvider(t.Context())
|
provider := (&oidc.ProviderConfig{}).NewProvider(context.TODO())
|
||||||
defaultAuth = &OIDCProvider{
|
defaultAuth = &OIDCProvider{
|
||||||
oauthConfig: &oauth2.Config{
|
oauthConfig: &oauth2.Config{
|
||||||
ClientID: "test-client",
|
ClientID: "test-client",
|
||||||
@@ -35,8 +36,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",
|
||||||
}),
|
}),
|
||||||
@@ -103,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(t.Context(), ts.Client())
|
providerCtx := oidc.ClientContext(context.Background(), 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{
|
||||||
@@ -149,17 +149,17 @@ func TestOIDCLoginHandler(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Success - Redirects to provider",
|
name: "Success - Redirects to provider",
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusTemporaryRedirect,
|
||||||
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, OIDCAuthInitPath, nil)
|
req := httptest.NewRequest(http.MethodGet, "/auth/redirect", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
defaultAuth.(*OIDCProvider).HandleAuth(w, req)
|
defaultAuth.RedirectLoginPage(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)
|
||||||
@@ -195,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.StatusFound,
|
wantStatus: http.StatusTemporaryRedirect,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Failure - Missing state",
|
name: "Failure - Missing state",
|
||||||
@@ -220,7 +220,7 @@ func TestOIDCCallbackHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
defaultAuth.(*OIDCProvider).PostAuthCallbackHandler(w, req)
|
defaultAuth.LoginCallbackHandler(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)
|
||||||
@@ -228,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, CookieOauthToken)
|
ExpectEqual(t, setCookie.Name, defaultAuth.TokenCookieName())
|
||||||
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)
|
||||||
@@ -271,6 +271,7 @@ 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,6 +280,7 @@ 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,
|
||||||
},
|
},
|
||||||
@@ -287,6 +289,7 @@ 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"},
|
||||||
@@ -297,13 +300,14 @@ 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.allowedUsers, tt.allowedGroups)
|
_, err := NewOIDCProvider(tt.issuerURL, tt.clientID, tt.clientSecret, tt.redirectURL, 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)
|
||||||
}
|
}
|
||||||
@@ -397,7 +401,7 @@ func TestCheckToken(t *testing.T) {
|
|||||||
"preferred_username": "user1",
|
"preferred_username": "user1",
|
||||||
"groups": []string{"group1"},
|
"groups": []string{"group1"},
|
||||||
},
|
},
|
||||||
wantErr: ErrInvalidOAuthToken,
|
wantErr: ErrInvalidToken,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Error - Server returns incorrect audience",
|
name: "Error - Server returns incorrect audience",
|
||||||
@@ -408,7 +412,7 @@ func TestCheckToken(t *testing.T) {
|
|||||||
"preferred_username": "user1",
|
"preferred_username": "user1",
|
||||||
"groups": []string{"group1"},
|
"groups": []string{"group1"},
|
||||||
},
|
},
|
||||||
wantErr: ErrInvalidOAuthToken,
|
wantErr: ErrInvalidToken,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Error - Server returns expired token",
|
name: "Error - Server returns expired token",
|
||||||
@@ -419,16 +423,13 @@ func TestCheckToken(t *testing.T) {
|
|||||||
"preferred_username": "user1",
|
"preferred_username": "user1",
|
||||||
"groups": []string{"group1"},
|
"groups": []string{"group1"},
|
||||||
},
|
},
|
||||||
wantErr: ErrInvalidOAuthToken,
|
wantErr: ErrInvalidToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
// Create the Auth Provider.
|
// Create the Auth Provider.
|
||||||
auth := &OIDCProvider{
|
auth := &OIDCProvider{
|
||||||
oauthConfig: &oauth2.Config{
|
|
||||||
ClientID: clientID,
|
|
||||||
},
|
|
||||||
oidcVerifier: provider.verifier,
|
oidcVerifier: provider.verifier,
|
||||||
allowedUsers: tc.allowedUsers,
|
allowedUsers: tc.allowedUsers,
|
||||||
allowedGroups: tc.allowedGroups,
|
allowedGroups: tc.allowedGroups,
|
||||||
@@ -438,7 +439,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.getAppScopedCookieName(CookieOauthToken),
|
Name: auth.TokenCookieName(),
|
||||||
Value: signedToken,
|
Value: signedToken,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -452,35 +453,3 @@ 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
internal/api/v1/auth/provider.go
Normal file
13
internal/api/v1/auth/provider.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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,11 +1,12 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/pkg/json"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
@@ -32,8 +33,6 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Provider = (*UserPassAuth)(nil)
|
|
||||||
|
|
||||||
func NewUserPassAuth(username, password string, secret []byte, tokenTTL time.Duration) (*UserPassAuth, error) {
|
func NewUserPassAuth(username, password string, secret []byte, tokenTTL time.Duration) (*UserPassAuth, error) {
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,7 +77,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 ErrMissingSessionToken
|
return ErrMissingToken
|
||||||
}
|
}
|
||||||
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) {
|
||||||
@@ -92,7 +91,7 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
|||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case !token.Valid:
|
case !token.Valid:
|
||||||
return ErrInvalidSessionToken
|
return ErrInvalidToken
|
||||||
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()):
|
||||||
@@ -102,21 +101,22 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserPassAuthCallbackRequest struct {
|
func (auth *UserPassAuth) RedirectLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||||
User string `json:"username"`
|
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
|
||||||
Pass string `json:"password"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
func (auth *UserPassAuth) LoginCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var creds UserPassAuthCallbackRequest
|
var creds struct {
|
||||||
|
User string `json:"username"`
|
||||||
|
Pass string `json:"password"`
|
||||||
|
}
|
||||||
err := json.NewDecoder(r.Body).Decode(&creds)
|
err := json.NewDecoder(r.Body).Decode(&creds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
gphttp.Unauthorized(w, "invalid credentials")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := auth.validatePassword(creds.User, creds.Pass); err != nil {
|
if err := auth.validatePassword(creds.User, creds.Pass); err != nil {
|
||||||
// NOTE: do not include the actual error here
|
gphttp.Unauthorized(w, "invalid credentials")
|
||||||
http.Error(w, "invalid credentials", http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
token, err := auth.NewToken()
|
token, err := auth.NewToken()
|
||||||
@@ -124,17 +124,12 @@ func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http
|
|||||||
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) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
func (auth *UserPassAuth) LogoutCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/login", http.StatusFound)
|
DefaultLogoutCallbackHandler(auth, w, r)
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
@@ -2,13 +2,14 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/pkg/json"
|
||||||
|
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -98,7 +99,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.PostAuthCallbackHandler(w, req)
|
auth.LoginCallbackHandler(w, req)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
ExpectEqual(t, w.Code, http.StatusUnauthorized)
|
ExpectEqual(t, w.Code, http.StatusUnauthorized)
|
||||||
} else {
|
} else {
|
||||||
70
internal/api/v1/auth/utils.go
Normal file
70
internal/api/v1/auth/utils.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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,72 +0,0 @@
|
|||||||
package certapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
"github.com/yusing/go-proxy/internal/logging/memlogger"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @x-id "renew"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Renew cert
|
|
||||||
// @Description Renew cert
|
|
||||||
// @Tags cert,websocket
|
|
||||||
// @Produce plain
|
|
||||||
// @Success 200 {object} apitypes.SuccessResponse
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /cert/renew [get]
|
|
||||||
func Renew(c *gin.Context) {
|
|
||||||
autocert := config.GetInstance().AutoCertProvider()
|
|
||||||
if autocert == nil {
|
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to create websocket manager"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer manager.Close()
|
|
||||||
|
|
||||||
logs, cancel := memlogger.Events()
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer close(done)
|
|
||||||
|
|
||||||
err = autocert.ObtainCert()
|
|
||||||
if err != nil {
|
|
||||||
gperr.LogError("failed to obtain cert", err)
|
|
||||||
_ = manager.WriteData(websocket.TextMessage, []byte(err.Error()), 10*time.Second)
|
|
||||||
} else {
|
|
||||||
log.Info().Msg("cert obtained successfully")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case l := <-logs:
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = manager.WriteData(websocket.TextMessage, l, 10*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,8 @@ package certapi
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/yusing/go-proxy/pkg/json"
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,29 +15,18 @@ type CertInfo struct {
|
|||||||
NotAfter int64 `json:"not_after"`
|
NotAfter int64 `json:"not_after"`
|
||||||
DNSNames []string `json:"dns_names"`
|
DNSNames []string `json:"dns_names"`
|
||||||
EmailAddresses []string `json:"email_addresses"`
|
EmailAddresses []string `json:"email_addresses"`
|
||||||
} // @name CertInfo
|
}
|
||||||
|
|
||||||
// @x-id "info"
|
func GetCertInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Get cert info
|
|
||||||
// @Description Get cert info
|
|
||||||
// @Tags cert
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} CertInfo
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 404 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /cert/info [get]
|
|
||||||
func Info(c *gin.Context) {
|
|
||||||
autocert := config.GetInstance().AutoCertProvider()
|
autocert := config.GetInstance().AutoCertProvider()
|
||||||
if autocert == nil {
|
if autocert == nil {
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
http.Error(w, "autocert is not enabled", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := autocert.GetCert(nil)
|
cert, err := autocert.GetCert(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to get cert info"))
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,5 +38,5 @@ func Info(c *gin.Context) {
|
|||||||
DNSNames: cert.Leaf.DNSNames,
|
DNSNames: cert.Leaf.DNSNames,
|
||||||
EmailAddresses: cert.Leaf.EmailAddresses,
|
EmailAddresses: cert.Leaf.EmailAddresses,
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, certInfo)
|
json.NewEncoder(w).Encode(&certInfo)
|
||||||
}
|
}
|
||||||
56
internal/api/v1/certapi/renew.go
Normal file
56
internal/api/v1/certapi/renew.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package certapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging/memlogger"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RenewCert(w http.ResponseWriter, r *http.Request) {
|
||||||
|
autocert := config.GetInstance().AutoCertProvider()
|
||||||
|
if autocert == nil {
|
||||||
|
http.Error(w, "autocert is not enabled", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := gpwebsocket.Initiate(w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//nolint:errcheck
|
||||||
|
defer conn.CloseNow()
|
||||||
|
|
||||||
|
logs, cancel := memlogger.Events()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
err = autocert.ObtainCert()
|
||||||
|
if err != nil {
|
||||||
|
gperr.LogError("failed to obtain cert", err)
|
||||||
|
gpwebsocket.WriteText(r, conn, err.Error())
|
||||||
|
} else {
|
||||||
|
logging.Info().Msg("cert obtained successfully")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case l := <-logs:
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !gpwebsocket.WriteText(r, conn, string(l)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
internal/api/v1/config_file.go
Normal file
132
internal/api/v1/config_file.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
||||||
|
"github.com/yusing/go-proxy/internal/route/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FileTypeConfig FileType = "config"
|
||||||
|
FileTypeProvider FileType = "provider"
|
||||||
|
FileTypeMiddleware FileType = "middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fileType(file string) FileType {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(path.Base(file), "config."):
|
||||||
|
return FileTypeConfig
|
||||||
|
case strings.HasPrefix(file, common.MiddlewareComposeDir):
|
||||||
|
return FileTypeMiddleware
|
||||||
|
}
|
||||||
|
return FileTypeProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t FileType) IsValid() bool {
|
||||||
|
switch t {
|
||||||
|
case FileTypeConfig, FileTypeProvider, FileTypeMiddleware:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t FileType) GetPath(filename string) string {
|
||||||
|
if t == FileTypeMiddleware {
|
||||||
|
return path.Join(common.MiddlewareComposeDir, filename)
|
||||||
|
}
|
||||||
|
return path.Join(common.ConfigDir, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
|
||||||
|
fileType = FileType(r.PathValue("type"))
|
||||||
|
if !fileType.IsValid() {
|
||||||
|
err = gphttp.ErrInvalidKey("type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filename = r.PathValue("filename")
|
||||||
|
if filename == "" {
|
||||||
|
err = gphttp.ErrMissingKey("filename")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fileType, filename, err := getArgs(r)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.BadRequest(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content, err := os.ReadFile(fileType.GetPath(filename))
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gphttp.WriteBody(w, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateFile(fileType FileType, content []byte) gperr.Error {
|
||||||
|
switch fileType {
|
||||||
|
case FileTypeConfig:
|
||||||
|
return config.Validate(content)
|
||||||
|
case FileTypeMiddleware:
|
||||||
|
errs := gperr.NewBuilder("middleware errors")
|
||||||
|
middleware.BuildMiddlewaresFromYAML("", content, errs)
|
||||||
|
return errs.Error()
|
||||||
|
}
|
||||||
|
return provider.Validate(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fileType := FileType(r.PathValue("type"))
|
||||||
|
if !fileType.IsValid() {
|
||||||
|
gphttp.BadRequest(w, "invalid file type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Body.Close()
|
||||||
|
if valErr := validateFile(fileType, content); valErr != nil {
|
||||||
|
gphttp.JSONError(w, valErr, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fileType, filename, err := getArgs(r)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.BadRequest(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if valErr := validateFile(fileType, content); valErr != nil {
|
||||||
|
gphttp.JSONError(w, valErr, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(fileType.GetPath(filename), content, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.ServerError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
75
internal/api/v1/debug/handler.go
Normal file
75
internal/api/v1/debug/handler.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
//go:build debug
|
||||||
|
|
||||||
|
package debugapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"iter"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/docker"
|
||||||
|
"github.com/yusing/go-proxy/internal/idlewatcher"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp/servemux"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp/server"
|
||||||
|
"github.com/yusing/go-proxy/internal/proxmox"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartServer(cfg config.ConfigInstance) {
|
||||||
|
srv := server.NewServer(server.Options{
|
||||||
|
Name: "debug",
|
||||||
|
HTTPAddr: "127.0.0.1:7777",
|
||||||
|
Handler: newHandler(cfg),
|
||||||
|
})
|
||||||
|
srv.Start(task.RootTask("debug_server", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
type debuggable interface {
|
||||||
|
MarshalMap() map[string]any
|
||||||
|
Key() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSortedSlice[T debuggable](data iter.Seq2[string, T]) []map[string]any {
|
||||||
|
s := make([]map[string]any, 0)
|
||||||
|
for _, v := range data {
|
||||||
|
m := v.MarshalMap()
|
||||||
|
m["key"] = v.Key()
|
||||||
|
s = append(s, m)
|
||||||
|
}
|
||||||
|
sort.Slice(s, func(i, j int) bool {
|
||||||
|
return s[i]["key"].(string) < s[j]["key"].(string)
|
||||||
|
})
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonHandler[T debuggable](getData iter.Seq2[string, T]) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gpwebsocket.DynamicJSONHandler(w, r, func() []map[string]any {
|
||||||
|
return toSortedSlice(getData)
|
||||||
|
}, 200*time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func iterMap[K comparable, V debuggable](m func() map[K]V) iter.Seq2[K, V] {
|
||||||
|
return func(yield func(K, V) bool) {
|
||||||
|
for k, v := range m() {
|
||||||
|
if !yield(k, v) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHandler(cfg config.ConfigInstance) http.Handler {
|
||||||
|
mux := servemux.NewServeMux(cfg)
|
||||||
|
mux.HandleFunc("GET", "/tasks", jsonHandler(task.AllTasks()))
|
||||||
|
mux.HandleFunc("GET", "/idlewatcher", jsonHandler(idlewatcher.Watchers()))
|
||||||
|
mux.HandleFunc("GET", "/agents", jsonHandler(agent.Agents.Iter))
|
||||||
|
mux.HandleFunc("GET", "/proxmox", jsonHandler(proxmox.Clients.Iter))
|
||||||
|
mux.HandleFunc("GET", "/docker", jsonHandler(iterMap(docker.Clients)))
|
||||||
|
return mux
|
||||||
|
}
|
||||||
11
internal/api/v1/debug/handler_production.go
Normal file
11
internal/api/v1/debug/handler_production.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !debug
|
||||||
|
|
||||||
|
package debugapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartServer(cfg config.ConfigInstance) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package dockerapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @x-id "container"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Get container
|
|
||||||
// @Description Get container by container id
|
|
||||||
// @Tags docker
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path string true "Container ID"
|
|
||||||
// @Success 200 {object} Container
|
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse "ID is required"
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /docker/container/{id} [get]
|
|
||||||
func GetContainer(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
if id == "" {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("id is required"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dockerHost, ok := docker.GetDockerHostByContainerID(id)
|
|
||||||
if !ok {
|
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := docker.NewClient(dockerHost)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
cont, err := client.ContainerInspect(c.Request.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var state ContainerState
|
|
||||||
if cont.State != nil {
|
|
||||||
state = cont.State.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, &Container{
|
|
||||||
Server: dockerHost,
|
|
||||||
Name: cont.Name,
|
|
||||||
ID: cont.ID,
|
|
||||||
Image: cont.Image,
|
|
||||||
State: state,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package dockerapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
dockerSystem "github.com/docker/docker/api/types/system"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type containerStats struct {
|
|
||||||
Total int `json:"total"`
|
|
||||||
Running int `json:"running"`
|
|
||||||
Paused int `json:"paused"`
|
|
||||||
Stopped int `json:"stopped"`
|
|
||||||
} // @name ContainerStats
|
|
||||||
|
|
||||||
type dockerInfo struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
ServerVersion string `json:"version"`
|
|
||||||
Containers containerStats `json:"containers"`
|
|
||||||
Images int `json:"images"`
|
|
||||||
NCPU int `json:"n_cpu"`
|
|
||||||
MemTotal string `json:"memory"`
|
|
||||||
} // @name ServerInfo
|
|
||||||
|
|
||||||
func toDockerInfo(info dockerSystem.Info) dockerInfo {
|
|
||||||
return dockerInfo{
|
|
||||||
Name: info.Name,
|
|
||||||
ServerVersion: info.ServerVersion,
|
|
||||||
Containers: containerStats{
|
|
||||||
Total: info.ContainersRunning,
|
|
||||||
Running: info.ContainersRunning,
|
|
||||||
Paused: info.ContainersPaused,
|
|
||||||
Stopped: info.ContainersStopped,
|
|
||||||
},
|
|
||||||
Images: info.Images,
|
|
||||||
NCPU: info.NCPU,
|
|
||||||
MemTotal: strutils.FormatByteSize(info.MemTotal),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @x-id "info"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Get docker info
|
|
||||||
// @Description Get docker info
|
|
||||||
// @Tags docker
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} dockerInfo
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /docker/info [get]
|
|
||||||
func Info(c *gin.Context) {
|
|
||||||
serveHTTP[dockerInfo](c, GetDockerInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, gperr.Error) {
|
|
||||||
errs := gperr.NewBuilder("failed to get docker info")
|
|
||||||
dockerInfos := make([]dockerInfo, len(dockerClients))
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
for name, dockerClient := range dockerClients {
|
|
||||||
info, err := dockerClient.Info(ctx)
|
|
||||||
if err != nil {
|
|
||||||
errs.Add(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
info.Name = name
|
|
||||||
dockerInfos[i] = toDockerInfo(info)
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(dockerInfos, func(i, j int) bool {
|
|
||||||
return dockerInfos[i].Name < dockerInfos[j].Name
|
|
||||||
})
|
|
||||||
return dockerInfos, errs.Error()
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
package dockerapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/pkg/stdcopy"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
|
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LogsQueryParams struct {
|
|
||||||
Stdout bool `form:"stdout,default=true"`
|
|
||||||
Stderr bool `form:"stderr,default=true"`
|
|
||||||
Since string `form:"from"`
|
|
||||||
Until string `form:"to"`
|
|
||||||
Levels string `form:"levels"`
|
|
||||||
} // @name LogsQueryParams
|
|
||||||
|
|
||||||
// @x-id "logs"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Get docker container logs
|
|
||||||
// @Description Get docker container logs by container id
|
|
||||||
// @Tags docker,websocket
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path string true "container id"
|
|
||||||
// @Param stdout query bool false "show stdout"
|
|
||||||
// @Param stderr query bool false "show stderr"
|
|
||||||
// @Param from query string false "from timestamp"
|
|
||||||
// @Param to query string false "to timestamp"
|
|
||||||
// @Param levels query string false "levels"
|
|
||||||
// @Success 200
|
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 404 {object} apitypes.ErrorResponse "server not found or container not found"
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /docker/logs/{id} [get]
|
|
||||||
func Logs(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
if id == "" {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("container id is required"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var queryParams LogsQueryParams
|
|
||||||
if err := c.ShouldBindQuery(&queryParams); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid query params"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: implement levels
|
|
||||||
dockerHost, ok := docker.GetDockerHostByContainerID(id)
|
|
||||||
if !ok {
|
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error(fmt.Sprintf("container %s not found", id)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dockerClient, err := docker.NewClient(dockerHost)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to get docker client"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer dockerClient.Close()
|
|
||||||
|
|
||||||
opts := container.LogsOptions{
|
|
||||||
ShowStdout: queryParams.Stdout,
|
|
||||||
ShowStderr: queryParams.Stderr,
|
|
||||||
Since: queryParams.Since,
|
|
||||||
Until: queryParams.Until,
|
|
||||||
Timestamps: true,
|
|
||||||
Follow: true,
|
|
||||||
Tail: "100",
|
|
||||||
}
|
|
||||||
if queryParams.Levels != "" {
|
|
||||||
opts.Details = true
|
|
||||||
}
|
|
||||||
|
|
||||||
logs, err := dockerClient.ContainerLogs(c.Request.Context(), id, opts)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to get container logs"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer logs.Close()
|
|
||||||
|
|
||||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to create websocket manager"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer manager.Close()
|
|
||||||
|
|
||||||
writer := manager.NewWriter(websocket.TextMessage)
|
|
||||||
|
|
||||||
_, err = stdcopy.StdCopy(writer, writer, logs) // de-multiplex logs
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, context.Canceled) || errors.Is(err, task.ErrProgramExiting) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Err(err).
|
|
||||||
Str("server", dockerHost).
|
|
||||||
Str("container", id).
|
|
||||||
Msg("failed to de-multiplex logs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package dockerapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @x-id "restart"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Restart container
|
|
||||||
// @Description Restart container by container id
|
|
||||||
// @Tags docker
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body StopRequest true "Request"
|
|
||||||
// @Success 200 {object} apitypes.SuccessResponse
|
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /docker/restart [post]
|
|
||||||
func Restart(c *gin.Context) {
|
|
||||||
var req StopRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
|
||||||
if !ok {
|
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := docker.NewClient(dockerHost)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
err = client.ContainerRestart(c.Request.Context(), req.ID, req.StopOptions)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to restart container"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, apitypes.Success("container restarted"))
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package dockerapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StartRequest struct {
|
|
||||||
ID string `json:"id" binding:"required"`
|
|
||||||
container.StartOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// @x-id "start"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Start container
|
|
||||||
// @Description Start container by container id
|
|
||||||
// @Tags docker
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body StartRequest true "Request"
|
|
||||||
// @Success 200 {object} apitypes.SuccessResponse
|
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /docker/start [post]
|
|
||||||
func Start(c *gin.Context) {
|
|
||||||
var req StartRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
|
||||||
if !ok {
|
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := docker.NewClient(dockerHost)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
err = client.ContainerStart(c.Request.Context(), req.ID, req.StartOptions)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to start container"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, apitypes.Success("container started"))
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package dockerapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StopRequest struct {
|
|
||||||
ID string `json:"id" binding:"required"`
|
|
||||||
container.StopOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// @x-id "stop"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Stop container
|
|
||||||
// @Description Stop container by container id
|
|
||||||
// @Tags docker
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body StopRequest true "Request"
|
|
||||||
// @Success 200 {object} apitypes.SuccessResponse
|
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /docker/stop [post]
|
|
||||||
func Stop(c *gin.Context) {
|
|
||||||
var req StopRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
|
||||||
if !ok {
|
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := docker.NewClient(dockerHost)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
err = client.ContainerStop(c.Request.Context(), req.ID, req.StopOptions)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, apitypes.Success("container stopped"))
|
|
||||||
}
|
|
||||||
5
internal/api/v1/dockerapi/common.go
Normal file
5
internal/api/v1/dockerapi/common.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package dockerapi
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const reqTimeout = 10 * time.Second
|
||||||
@@ -2,35 +2,23 @@ package dockerapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContainerState = container.ContainerState // @name ContainerState
|
|
||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
Server string `json:"server"`
|
Server string `json:"server"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
State ContainerState `json:"state,omitempty" extensions:"x-nullable"`
|
State string `json:"state"`
|
||||||
} // @name ContainerResponse
|
}
|
||||||
|
|
||||||
// @x-id "containers"
|
func Containers(w http.ResponseWriter, r *http.Request) {
|
||||||
// @BasePath /api/v1
|
serveHTTP[Container](w, r, GetContainers)
|
||||||
// @Summary Get containers
|
|
||||||
// @Description Get containers
|
|
||||||
// @Tags docker
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {array} Container
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /docker/containers [get]
|
|
||||||
func Containers(c *gin.Context) {
|
|
||||||
serveHTTP[Container](c, GetContainers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {
|
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {
|
||||||
57
internal/api/v1/dockerapi/info.go
Normal file
57
internal/api/v1/dockerapi/info.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package dockerapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/pkg/json"
|
||||||
|
|
||||||
|
dockerSystem "github.com/docker/docker/api/types/system"
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dockerInfo dockerSystem.Info
|
||||||
|
|
||||||
|
func (d *dockerInfo) MarshalJSONTo(buf []byte) []byte {
|
||||||
|
return json.MarshalTo(map[string]any{
|
||||||
|
"name": d.Name,
|
||||||
|
"version": d.ServerVersion,
|
||||||
|
"containers": map[string]int{
|
||||||
|
"total": d.Containers,
|
||||||
|
"running": d.ContainersRunning,
|
||||||
|
"paused": d.ContainersPaused,
|
||||||
|
"stopped": d.ContainersStopped,
|
||||||
|
},
|
||||||
|
"images": d.Images,
|
||||||
|
"n_cpu": d.NCPU,
|
||||||
|
"memory": strutils.FormatByteSize(d.MemTotal),
|
||||||
|
}, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DockerInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
serveHTTP[dockerInfo](w, r, GetDockerInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, gperr.Error) {
|
||||||
|
errs := gperr.NewBuilder("failed to get docker info")
|
||||||
|
dockerInfos := make([]dockerInfo, len(dockerClients))
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for name, dockerClient := range dockerClients {
|
||||||
|
info, err := dockerClient.Info(ctx)
|
||||||
|
if err != nil {
|
||||||
|
errs.Add(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info.Name = name
|
||||||
|
dockerInfos[i] = dockerInfo(info)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(dockerInfos, func(i, j int) bool {
|
||||||
|
return dockerInfos[i].Name < dockerInfos[j].Name
|
||||||
|
})
|
||||||
|
return dockerInfos, errs.Error()
|
||||||
|
}
|
||||||
69
internal/api/v1/dockerapi/logs.go
Normal file
69
internal/api/v1/dockerapi/logs.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package dockerapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"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) {
|
||||||
|
query := r.URL.Query()
|
||||||
|
server := r.PathValue("server")
|
||||||
|
containerID := r.PathValue("container")
|
||||||
|
stdout := strutils.ParseBool(query.Get("stdout"))
|
||||||
|
stderr := strutils.ParseBool(query.Get("stderr"))
|
||||||
|
since := query.Get("from")
|
||||||
|
until := query.Get("to")
|
||||||
|
levels := query.Get("levels") // TODO: implement levels
|
||||||
|
|
||||||
|
dockerClient, found, err := getDockerClient(server)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.BadRequest(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
gphttp.NotFound(w, "server not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := container.LogsOptions{
|
||||||
|
ShowStdout: stdout,
|
||||||
|
ShowStderr: stderr,
|
||||||
|
Since: since,
|
||||||
|
Until: until,
|
||||||
|
Timestamps: true,
|
||||||
|
Follow: true,
|
||||||
|
Tail: "100",
|
||||||
|
}
|
||||||
|
if levels != "" {
|
||||||
|
opts.Details = true
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := dockerClient.ContainerLogs(r.Context(), containerID, opts)
|
||||||
|
if err != nil {
|
||||||
|
gphttp.BadRequest(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer logs.Close()
|
||||||
|
|
||||||
|
conn, err := gpwebsocket.Initiate(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.CloseNow()
|
||||||
|
|
||||||
|
writer := gpwebsocket.NewWriter(r.Context(), conn, websocket.MessageText)
|
||||||
|
_, err = stdcopy.StdCopy(writer, writer, logs) // de-multiplex logs
|
||||||
|
if err != nil {
|
||||||
|
logging.Err(err).
|
||||||
|
Str("server", server).
|
||||||
|
Str("container", containerID).
|
||||||
|
Msg("failed to de-multiplex logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,14 +5,16 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/yusing/go-proxy/pkg/json"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
"github.com/coder/websocket/wsjson"
|
||||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
"github.com/yusing/go-proxy/internal/docker"
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"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/net/gphttp/websocket"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -44,13 +46,13 @@ func getDockerClients() (DockerClients, gperr.Error) {
|
|||||||
dockerClients[name] = dockerClient
|
dockerClients[name] = dockerClient
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, agent := range agent.ListAgents() {
|
for _, agent := range agent.Agents.Iter {
|
||||||
dockerClient, err := docker.NewClient(agent.FakeDockerHost())
|
dockerClient, err := docker.NewClient(agent.FakeDockerHost())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
connErrs.Add(err)
|
connErrs.Add(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dockerClients[agent.Name] = dockerClient
|
dockerClients[agent.Name()] = dockerClient
|
||||||
}
|
}
|
||||||
|
|
||||||
return dockerClients, connErrs.Error()
|
return dockerClients, connErrs.Error()
|
||||||
@@ -65,12 +67,10 @@ func getDockerClient(server string) (*docker.SharedClient, bool, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if host == "" {
|
for _, agent := range agent.Agents.Iter {
|
||||||
for _, agent := range agent.ListAgents() {
|
if agent.Name() == server {
|
||||||
if agent.Name == server {
|
host = agent.FakeDockerHost()
|
||||||
host = agent.FakeDockerHost()
|
break
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if host == "" {
|
if host == "" {
|
||||||
@@ -92,30 +92,35 @@ func closeAllClients(dockerClients DockerClients) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleResult[V any, T ResultType[V]](c *gin.Context, errs error, result T) {
|
func handleResult[V any, T ResultType[V]](w http.ResponseWriter, errs error, result T) {
|
||||||
if errs != nil {
|
if errs != nil {
|
||||||
|
gperr.LogError("docker errors", errs)
|
||||||
if len(result) == 0 {
|
if len(result) == 0 {
|
||||||
c.Error(apitypes.InternalServerError(errs, "docker errors"))
|
http.Error(w, "docker errors", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, result)
|
json.NewEncoder(w).Encode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveHTTP[V any, T ResultType[V]](c *gin.Context, 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)) {
|
||||||
dockerClients, err := getDockerClients()
|
dockerClients, err := getDockerClients()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleResult[V, T](c, err, nil)
|
handleResult[V, T](w, err, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer closeAllClients(dockerClients)
|
defer closeAllClients(dockerClients)
|
||||||
|
|
||||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
if httpheaders.IsWebsocket(r.Header) {
|
||||||
websocket.PeriodicWrite(c, 5*time.Second, func() (any, error) {
|
gpwebsocket.Periodic(w, r, 5*time.Second, func(conn *websocket.Conn) error {
|
||||||
return getResult(c.Request.Context(), dockerClients)
|
result, err := getResult(r.Context(), dockerClients)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return wsjson.Write(r.Context(), conn, result)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
result, err := getResult(c.Request.Context(), dockerClients)
|
result, err := getResult(r.Context(), dockerClients)
|
||||||
handleResult[V](c, err, result)
|
handleResult[V](w, err, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,91 +0,0 @@
|
|||||||
package v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/homepage"
|
|
||||||
"github.com/yusing/go-proxy/internal/route/routes"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GetFavIconRequest struct {
|
|
||||||
URL string `form:"url" binding:"required_without=Alias"`
|
|
||||||
Alias string `form:"alias" binding:"required_without=URL"`
|
|
||||||
} // @name GetFavIconRequest
|
|
||||||
|
|
||||||
// @x-id "favicon"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Get favicon
|
|
||||||
// @Description Get favicon
|
|
||||||
// @Tags v1
|
|
||||||
// @Accept json
|
|
||||||
// @Produce image/svg+xml,image/x-icon,image/png,image/webp
|
|
||||||
// @Param url query string false "URL of the route"
|
|
||||||
// @Param alias query string false "Alias of the route"
|
|
||||||
// @Success 200 {array} homepage.FetchResult
|
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse "Bad Request: alias is empty or route is not HTTPRoute"
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden: unauthorized"
|
|
||||||
// @Failure 404 {object} apitypes.ErrorResponse "Not Found: route or icon not found"
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal Server Error: internal error"
|
|
||||||
// @Router /favicon [get]
|
|
||||||
func FavIcon(c *gin.Context) {
|
|
||||||
var request GetFavIconRequest
|
|
||||||
if err := c.ShouldBindQuery(&request); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// try with url
|
|
||||||
if request.URL != "" {
|
|
||||||
var iconURL homepage.IconURL
|
|
||||||
if err := iconURL.Parse(request.URL); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid url", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fetchResult := homepage.FetchFavIconFromURL(c.Request.Context(), &iconURL)
|
|
||||||
if !fetchResult.OK() {
|
|
||||||
c.JSON(fetchResult.StatusCode, apitypes.Error(fetchResult.ErrMsg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Data(fetchResult.StatusCode, fetchResult.ContentType(), fetchResult.Icon)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// try with alias
|
|
||||||
result := GetFavIconFromAlias(c.Request.Context(), request.Alias)
|
|
||||||
if !result.OK() {
|
|
||||||
c.JSON(result.StatusCode, apitypes.Error(result.ErrMsg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Data(result.StatusCode, result.ContentType(), result.Icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetFavIconFromAlias(ctx context.Context, alias string) *homepage.FetchResult {
|
|
||||||
// try with route.Icon
|
|
||||||
r, ok := routes.HTTP.Get(alias)
|
|
||||||
if !ok {
|
|
||||||
return &homepage.FetchResult{
|
|
||||||
StatusCode: http.StatusNotFound,
|
|
||||||
ErrMsg: "route not found",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result *homepage.FetchResult
|
|
||||||
hp := r.HomepageItem()
|
|
||||||
if hp.Icon != nil {
|
|
||||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
|
||||||
result = homepage.FindIcon(ctx, r, *hp.Icon.FullURL)
|
|
||||||
} else {
|
|
||||||
result = homepage.FetchFavIconFromURL(ctx, hp.Icon)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// try extract from "link[rel=icon]"
|
|
||||||
result = homepage.FindIcon(ctx, r, "/")
|
|
||||||
}
|
|
||||||
if result.StatusCode == 0 {
|
|
||||||
result.StatusCode = http.StatusOK
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
77
internal/api/v1/favicon/favicon.go
Normal file
77
internal/api/v1/favicon/favicon.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package favicon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/gperr"
|
||||||
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||||
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetFavIcon returns the favicon of the route
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - 200 OK: if icon found
|
||||||
|
// - 400 Bad Request: if alias is empty or route is not HTTPRoute
|
||||||
|
// - 404 Not Found: if route or icon not found
|
||||||
|
// - 500 Internal Server Error: if internal error
|
||||||
|
// - others: depends on route handler response
|
||||||
|
func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||||
|
url, alias := req.FormValue("url"), req.FormValue("alias")
|
||||||
|
if url == "" && alias == "" {
|
||||||
|
gphttp.ClientError(w, gphttp.ErrMissingKey("url or alias"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if url != "" && alias != "" {
|
||||||
|
gphttp.ClientError(w, gperr.New("url and alias are mutually exclusive"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// try with url
|
||||||
|
if url != "" {
|
||||||
|
var iconURL homepage.IconURL
|
||||||
|
if err := iconURL.Parse(url); err != nil {
|
||||||
|
gphttp.ClientError(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetchResult := homepage.FetchFavIconFromURL(&iconURL)
|
||||||
|
if !fetchResult.OK() {
|
||||||
|
http.Error(w, fetchResult.ErrMsg, fetchResult.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", fetchResult.ContentType())
|
||||||
|
gphttp.WriteBody(w, fetchResult.Icon)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// try with route.Icon
|
||||||
|
r, ok := routes.GetHTTPRoute(alias)
|
||||||
|
if !ok {
|
||||||
|
gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var result *homepage.FetchResult
|
||||||
|
hp := r.HomepageItem()
|
||||||
|
if hp.Icon != nil {
|
||||||
|
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||||
|
result = homepage.FindIcon(req.Context(), r, hp.Icon.Value)
|
||||||
|
} else {
|
||||||
|
result = homepage.FetchFavIconFromURL(hp.Icon)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// try extract from "link[rel=icon]"
|
||||||
|
result = homepage.FindIcon(req.Context(), r, "/")
|
||||||
|
}
|
||||||
|
if result.StatusCode == 0 {
|
||||||
|
result.StatusCode = http.StatusOK
|
||||||
|
}
|
||||||
|
if !result.OK() {
|
||||||
|
http.Error(w, result.ErrMsg, result.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", result.ContentType())
|
||||||
|
gphttp.WriteBody(w, result.Icon)
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package fileapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FileType string // @name FileType
|
|
||||||
|
|
||||||
const (
|
|
||||||
FileTypeConfig FileType = "config" // @name FileTypeConfig
|
|
||||||
FileTypeProvider FileType = "provider" // @name FileTypeProvider
|
|
||||||
FileTypeMiddleware FileType = "middleware" // @name FileTypeMiddleware
|
|
||||||
)
|
|
||||||
|
|
||||||
type GetFileContentRequest struct {
|
|
||||||
FileType FileType `form:"type" binding:"required,oneof=config provider middleware"`
|
|
||||||
Filename string `form:"filename" binding:"required" format:"filename"`
|
|
||||||
} // @name GetFileContentRequest
|
|
||||||
|
|
||||||
// @x-id "get"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Get file content
|
|
||||||
// @Description Get file content
|
|
||||||
// @Tags file
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json,application/godoxy+yaml
|
|
||||||
// @Param query query GetFileContentRequest true "Request"
|
|
||||||
// @Success 200 {string} application/godoxy+yaml "File content"
|
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /file/content [get]
|
|
||||||
func Get(c *gin.Context) {
|
|
||||||
var request GetFileContentRequest
|
|
||||||
if err := c.ShouldBindQuery(&request); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := os.ReadFile(request.FileType.GetPath(request.Filename))
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to read file"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// RFC 9512: https://www.rfc-editor.org/rfc/rfc9512.html
|
|
||||||
// xxx/yyy+yaml
|
|
||||||
c.Data(http.StatusOK, "application/godoxy+yaml", content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetFileType(file string) FileType {
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(path.Base(file), "config."):
|
|
||||||
return FileTypeConfig
|
|
||||||
case strings.HasPrefix(file, common.MiddlewareComposeBasePath):
|
|
||||||
return FileTypeMiddleware
|
|
||||||
}
|
|
||||||
return FileTypeProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t FileType) GetPath(filename string) string {
|
|
||||||
if t == FileTypeMiddleware {
|
|
||||||
return path.Join(common.MiddlewareComposeBasePath, filename)
|
|
||||||
}
|
|
||||||
return path.Join(common.ConfigBasePath, filename)
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package fileapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ListFilesResponse struct {
|
|
||||||
Config []string `json:"config"`
|
|
||||||
Provider []string `json:"provider"`
|
|
||||||
Middleware []string `json:"middleware"`
|
|
||||||
} // @name ListFilesResponse
|
|
||||||
|
|
||||||
// @x-id "list"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary List files
|
|
||||||
// @Description List files
|
|
||||||
// @Tags file
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} ListFilesResponse
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /file/list [get]
|
|
||||||
func List(c *gin.Context) {
|
|
||||||
resp := map[FileType][]string{
|
|
||||||
FileTypeConfig: make([]string, 0),
|
|
||||||
FileTypeProvider: make([]string, 0),
|
|
||||||
FileTypeMiddleware: make([]string, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
// config/
|
|
||||||
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to list files"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
t := GetFileType(file)
|
|
||||||
file = strings.TrimPrefix(file, common.ConfigBasePath+"/")
|
|
||||||
resp[t] = append(resp[t], file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// config/middlewares/
|
|
||||||
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to list files"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, mid := range mids {
|
|
||||||
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
|
|
||||||
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package fileapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SetFileContentRequest GetFileContentRequest
|
|
||||||
|
|
||||||
// @x-id "set"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Set file content
|
|
||||||
// @Description Set file content
|
|
||||||
// @Tags file
|
|
||||||
// @Accept text/plain
|
|
||||||
// @Produce json
|
|
||||||
// @Param type query FileType true "Type"
|
|
||||||
// @Param filename query string true "Filename"
|
|
||||||
// @Param file body string true "File"
|
|
||||||
// @Success 200 {object} apitypes.SuccessResponse
|
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /file/content [put]
|
|
||||||
func Set(c *gin.Context) {
|
|
||||||
var request SetFileContentRequest
|
|
||||||
if err := c.ShouldBindQuery(&request); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := c.GetRawData()
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to read file"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if valErr := validateFile(request.FileType, content); valErr != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid file", valErr))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(request.FileType.GetPath(request.Filename), content, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to write file"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, apitypes.Success("file set"))
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package fileapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
|
||||||
"github.com/yusing/go-proxy/internal/gperr"
|
|
||||||
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
|
||||||
"github.com/yusing/go-proxy/internal/route/provider"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ValidateFileRequest struct {
|
|
||||||
FileType FileType `form:"type" validate:"required,oneof=config provider middleware"`
|
|
||||||
} // @name ValidateFileRequest
|
|
||||||
|
|
||||||
// @x-id "validate"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Validate file
|
|
||||||
// @Description Validate file
|
|
||||||
// @Tags file
|
|
||||||
// @Accept text/plain
|
|
||||||
// @Produce json
|
|
||||||
// @Param type query FileType true "Type"
|
|
||||||
// @Param file body string true "File content"
|
|
||||||
// @Success 200 {object} apitypes.SuccessResponse "File validated"
|
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse "Bad request"
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden"
|
|
||||||
// @Failure 417 {object} any "Validation failed"
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
|
||||||
// @Router /file/validate [post]
|
|
||||||
func Validate(c *gin.Context) {
|
|
||||||
var request ValidateFileRequest
|
|
||||||
if err := c.ShouldBindQuery(&request); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := c.GetRawData()
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to read file"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Request.Body.Close()
|
|
||||||
|
|
||||||
if valErr := validateFile(request.FileType, content); valErr != nil {
|
|
||||||
c.JSON(http.StatusExpectationFailed, valErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, apitypes.Success("file validated"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateFile(fileType FileType, content []byte) gperr.Error {
|
|
||||||
switch fileType {
|
|
||||||
case FileTypeConfig:
|
|
||||||
return config.Validate(content)
|
|
||||||
case FileTypeMiddleware:
|
|
||||||
errs := gperr.NewBuilder("middleware errors")
|
|
||||||
middleware.BuildMiddlewaresFromYAML("", content, errs)
|
|
||||||
return errs.Error()
|
|
||||||
}
|
|
||||||
return provider.Validate(content)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user