Compare commits

..

149 commits

Author SHA1 Message Date
yusing
cee6eaecff fix(healthcheck): retry on error and stop afte 5 trials 2025-05-18 22:16:12 +08:00
yusing
67a6b89ea5 fix(agent): improve install script command handling and add agent running check
Some checks failed
Docker Image CI (nightly) / build-nightly (push) Has been cancelled
Docker Image CI (nightly) / build-nightly-agent (push) Has been cancelled
2025-05-17 08:51:22 +08:00
yusing
78be9b1c71 fix(agent): update script url 2025-05-17 08:39:03 +08:00
yusing
26856b612a docs: replace broken/old links and update README_CHT key features section 2025-05-17 08:38:40 +08:00
yusing
36ceba3ae7 security: switch from RSA-2048 to ECDSA-P256 for agent certs and update certificate config and handling 2025-05-17 08:29:01 +08:00
yusing
f45f3fba79 refactor: logic refactor for setting xff header
Some checks are pending
Docker Image CI (nightly) / build-nightly (push) Waiting to run
Docker Image CI (nightly) / build-nightly-agent (push) Waiting to run
2025-05-16 20:14:03 +08:00
yusing
4bbff323e3 chore: update dependencies
Some checks are pending
Docker Image CI (nightly) / build-nightly (push) Waiting to run
Docker Image CI (nightly) / build-nightly-agent (push) Waiting to run
2025-05-16 07:19:33 +08:00
yusing
2e68baa93e tweak: optimize memory allocation and increase throughput 2025-05-16 07:15:45 +08:00
yusing
a162371ec5 feat: parallelize system info collection and refactor code 2025-05-14 21:38:28 +08:00
yusing
8f9c76daa5 chore: update dependencies 2025-05-14 21:00:53 +08:00
yusing
8b3e058885 fix: error formatting 2025-05-14 20:34:41 +08:00
yusing
023cbc81bc ci: update Docker CI workflows to exclude tags for socket-proxy and improve caching 2025-05-14 13:50:12 +08:00
yusing
b490e8c475 fix(acl): maxmind error even if configured, refactor 2025-05-14 13:44:43 +08:00
yusing
8e27886235 fix: incorrect unmarshal behavior for pointer primitives 2025-05-14 12:20:52 +08:00
yusing
7435b8e485 tests: add test for acl matchers 2025-05-13 20:11:16 +08:00
yusing
21724c037f fix: error formatting 2025-05-13 20:11:03 +08:00
yusing
44b4cff35e fix: acl matcher parsing, refactor 2025-05-13 19:40:43 +08:00
yusing
1e24765b17 fix: nil when printing error in edge cases 2025-05-13 19:40:04 +08:00
yusing
a1f2a84a16 fix(oidc): multiple state cookies being sent to frontend causing invalid oauth state
Some checks failed
Docker Image CI (nightly) / build-nightly (push) Has been cancelled
Docker Image CI (nightly) / build-nightly-agent (push) Has been cancelled
2025-05-12 14:19:18 +08:00
yusing
453262832a security: disallow tls1.0/1.1 2025-05-12 12:22:52 +08:00
yusing
99e975145c tweak default docker compose 2025-05-11 23:40:38 +08:00
yusing
e300170c51 fix: route autoconfiguration 2025-05-11 21:38:43 +08:00
yusing
1382137f20 tweak(cicd): attempt on better build caching 2025-05-11 07:00:34 +08:00
yusing
54d7508f5d style: gofmt and fix golangcl-ilint
Some checks failed
Docker Image CI (nightly) / build-nightly (push) Has been cancelled
Docker Image CI (nightly) / build-nightly-agent (push) Has been cancelled
2025-05-11 06:34:35 +08:00
yusing
71ca8c738e fix: middleware bypass 2025-05-11 06:33:22 +08:00
yusing
f1eefde964 fix(oidc): add timeout to oidc initialization 2025-05-11 05:58:18 +08:00
yusing
84e7a6591e fix(agent): health check logic 2025-05-11 00:05:01 +08:00
yusing
30c76cfc5f refactor: health check logic 2025-05-10 22:55:20 +08:00
yusing
a8ba42e360 fix: routes iter missing stream
Some checks are pending
Docker Image CI (nightly) / build-nightly (push) Waiting to run
Docker Image CI (nightly) / build-nightly-agent (push) Waiting to run
2025-05-10 21:31:38 +08:00
yusing
cd291556fc fix(oid); redirect 2025-05-10 21:25:27 +08:00
yusing
0d41809630 fix(middleware): move bypass after finalize 2025-05-10 21:19:03 +08:00
yusing
53acf75c04 fix(homepage): item not hiding after config override 2025-05-10 18:14:10 +08:00
yusing
cf30fe6cfc feat(homepage): custom app sort order 2025-05-10 17:36:51 +08:00
yusing
55bbcae911 feat(api): refined list route api 2025-05-10 15:22:30 +08:00
yusing
b30c0d7dc0 feat(api): include agent version in response
Some checks failed
Docker Image CI (nightly) / build-nightly (push) Waiting to run
Docker Image CI (nightly) / build-nightly-agent (push) Waiting to run
Docker Image CI (socket-proxy) / build (push) Has been cancelled
2025-05-10 13:37:51 +08:00
yusing
198ae2cd02 refactor(api): restructure existing routes and remove unused debug endpoints and command line arguments 2025-05-10 13:12:41 +08:00
yusing
26938eb6ed feat(api): add new route for listing routes by provider 2025-05-10 12:58:37 +08:00
yusing
48823a860f fix(docker-compose): remove default proxy.exclude
Some checks failed
Docker Image CI (socket-proxy) / build (push) Has been cancelled
2025-05-10 12:28:08 +08:00
yusing
985ff0a74d fix(deps): use dummy version for go-proxy module 2025-05-10 12:27:48 +08:00
yusing
43b493c60e fix(agent): docker handler 2025-05-10 12:26:50 +08:00
yusing
e0e0fab127 fix(agent): disable socket proxy by default 2025-05-10 12:26:06 +08:00
yusing
fc0dbd940c fix: Dockerfile caching 2025-05-10 12:12:39 +08:00
yusing
0208e6286f fix: docker socket handler 2025-05-10 11:24:28 +08:00
yusing
2c0b68c8c2 fix(build): Dockerfile 2025-05-10 10:50:26 +08:00
yusing
c05059765d style: coed cleanup and fix styling 2025-05-10 10:46:31 +08:00
yusing
a06787593c style: update golangci-lint and trunk configurations 2025-05-10 10:46:03 +08:00
yusing
8fe94d6d14 feat(socket-proxy): implement Docker socket proxy and related configurations
- Updated Dockerfile and Makefile for socket-proxy build.
- Modified go.mod to include necessary dependencies.
- Updated CI workflows for socket-proxy integration.
- Better module isolation
- Code refactor
2025-05-10 09:47:03 +08:00
yusing
4ddfb48b9d fix(setup): skipped autocert configuration
Some checks are pending
Docker Image CI (nightly) / build-nightly (push) Waiting to run
Docker Image CI (nightly) / build-nightly-agent (push) Waiting to run
2025-05-09 14:31:32 +08:00
yusing
31dc112591 fix(middleware): middleware chain error handling
- Removed unnecessary initialization of befores and modResps in middlewareChain.
- modifyResponse should return immediately on error.
2025-05-09 12:29:50 +08:00
yusing
6797897814 fix(healthcheck): ensure detail is included on error
Some checks are pending
Docker Image CI (nightly) / build-nightly (push) Waiting to run
Docker Image CI (nightly) / build-nightly-agent (push) Waiting to run
2025-05-09 12:26:31 +08:00
yusing
99eccd0b95 fix(monitor): reduce health check result initialization 2025-05-09 12:14:34 +08:00
yusing
0387739b94 fix(homepage): prioritize container name and alias as display name 2025-05-09 11:42:33 +08:00
yusing
ead27c72f1 fix(agent): typo for /distribution endpoint and update related configurations 2025-05-09 11:37:41 +08:00
yusing
455a85e6a0 feat(docker): add Docker socket proxy support and related configurations
Some checks are pending
Docker Image CI (nightly) / build-nightly (push) Waiting to run
Docker Image CI (nightly) / build-nightly-agent (push) Waiting to run
- Introduced Docker socket proxy handling in the agent.
- Added environment variables for Docker socket configuration.
- Implemented new Docker handler with endpoint permissions based on environment settings.
- Added tests for Docker handler functionality.
- Updated go.mod to include gorilla/mux for routing.
2025-05-08 20:59:32 +08:00
yusing
8424fd9f1a chore: upgrade dependencies 2025-05-08 17:57:08 +08:00
yusing
75ee0e63bd fix(middleware): fix route bypass matching
- replace upstream headers approach with context value
2025-05-08 17:49:36 +08:00
yusing
1ce607029a Merge branch 'main' into dev 2025-05-07 23:27:02 +08:00
yusing
1e80ad2a44 fix(docker): host network_mode port selection 2025-05-07 23:26:51 +08:00
yusing
4daefa19d1 build: update Go version to 1.24.3 in Dockerfile and go.mod 2025-05-07 23:12:55 +08:00
yusing
491231e439 Merge branch 'main' into dev 2025-05-06 20:27:37 +08:00
yusing
c90ec8caa1 feat(container): add UpdatePorts method and support for host network mode 2025-05-06 20:27:25 +08:00
yusing
9eb674029e tweak(logging): rename write count variable and adjust buffer check interval 2025-05-05 20:59:43 +08:00
yusing
e41c6530ab chore: update dependencies and Makefile 2025-05-05 20:41:25 +08:00
yusing
afd35c183d test: fix failed tests after code changes 2025-05-05 20:41:25 +08:00
yusing
f190483b4e feat(rules.on): support route directive 2025-05-05 20:41:25 +08:00
yusing
7b0ed09772 fix(error): self referencing 2025-05-05 20:41:25 +08:00
yusing
4415bffc35 feat(rules.on): support & as logical AND 2025-05-05 20:41:25 +08:00
yusing
ddab2766b4 feat(middlewares): middleware bypass rules 2025-05-05 20:41:25 +08:00
yusing
ef95682116 feat(rules): compile path rules directly to glob 2025-05-05 20:41:25 +08:00
yusing
dd65a8d04b style: replace for loops with slices.Contains 2025-05-05 20:41:25 +08:00
yusing
aa23b5b595 test: add unit tests for FormatByteSize function 2025-05-05 20:41:25 +08:00
yusing
c55c6c84bc feat(health): add health check detail to health api 2025-05-05 20:41:25 +08:00
yusing
a45e5e17db chore: update dependencies and Makefile 2025-05-05 20:39:05 +08:00
yusing
b8c0961de3 test: fix failed tests after code changes 2025-05-05 20:05:47 +08:00
yusing
62d3d200e6 feat(rules.on): support route directive 2025-05-05 19:34:24 +08:00
yusing
bf32cafd90 fix(error): self referencing 2025-05-05 19:32:55 +08:00
yusing
1c182b5a7d feat(rules.on): support & as logical AND 2025-05-05 19:15:35 +08:00
yusing
ad60f377ba feat(middlewares): middleware bypass rules 2025-05-05 18:01:07 +08:00
yusing
75db09b1f3 feat(rules): compile path rules directly to glob 2025-05-05 14:42:55 +08:00
yusing
6dd849f480 style: replace for loops with slices.Contains 2025-05-05 13:36:08 +08:00
yusing
e2ae29795d test: add unit tests for FormatByteSize function 2025-05-05 13:27:51 +08:00
vSLY
92fa0f8168 Update README.md (#104)
Clarify setup process
2025-05-05 13:27:25 +08:00
yusing
b090598b68 feat(health): add health check detail to health api 2025-05-05 13:27:00 +08:00
vSLY
2cec88d3ce
Update README.md (#104)
Clarify setup process
2025-05-05 00:45:29 +08:00
yusing
4df31263b5 fix(sensor): ignore "no data available" error 2025-05-05 00:33:43 +08:00
yusing
9eae809690 chore: move middleware trace to trace level 2025-05-04 23:58:47 +08:00
yusing
f1ba554a24 fix(notif): http 204 treated as error 2025-05-04 23:54:16 +08:00
yusing
f9a8aede20 feat: hCaptcha middleware 2025-05-04 17:21:12 +08:00
yusing
e275ee634c fix(http): content type detection 2025-05-04 16:18:46 +08:00
yusing
797d88772f fix: timeout waiting for maxmind db on shutdown 2025-05-04 16:04:17 +08:00
yusing
8ef8015a7f feat: improved icon and category lookup mechanism 2025-05-04 09:37:15 +08:00
yusing
5fce4b445b fix: error formatting 2025-05-04 07:12:28 +08:00
yusing
7552a706a7 chore: deps upgrade 2025-05-04 06:33:08 +08:00
yusing
e1bc6d1f44 fix: nil panic when formatting error 2025-05-04 06:33:00 +08:00
yusing
56850a9580 fix: update http error handling 2025-05-04 02:34:34 +08:00
yusing
5f780f4902 feat: improved port selection 2025-05-04 01:32:01 +08:00
yusing
ccb4639f43 breaking: move maxmind config to config.providers
- moved maxmind to separate module
- code refactored
- simplified test
2025-05-03 20:58:09 +08:00
yusing
ac1470d81d fix: remove incorrect comment from getOAuthRefreshToken function 2025-05-03 19:38:02 +08:00
yusing
efaabfa63a fix: access log field names 2025-05-03 19:32:02 +08:00
yusing
9043cf25c5 feat: push notification for config errors 2025-05-03 17:41:50 +08:00
yusing
98e90d7a0b refactor: improve error handling and response formatting in API 2025-05-03 17:41:10 +08:00
yusing
82c829de18 feat: notifications retry mechanism and improved error formatting 2025-05-03 14:30:40 +08:00
yusing
2fe4fef779 fix(oidc): enforce https redirection to prevent errors 2025-05-03 04:56:32 +08:00
yusing
91302ceed7 feat: simplify and optimize system info 2025-05-02 10:31:04 +08:00
yusing
7fa7b55b18 feat: notify for cert renewal result 2025-05-02 05:55:34 +08:00
yusing
69ee8495d8 refactor: notifications 2025-05-02 05:51:15 +08:00
yusing
28d9a72908 fix: icon not exists error on loading icon cache 2025-05-02 05:19:16 +08:00
yusing
770c698332 fix(idlewatcher): "unexpected container action" after unexpected EOF 2025-05-02 04:56:27 +08:00
yusing
cd4c843025 feat: light/dark variant icons and selfh.st tag as default category
- code refactor
- reduce memory usage
2025-05-02 03:38:50 +08:00
yusing
f0cf89060b chore(logging): move "adjusted buffer size" to debug level 2025-05-01 23:42:45 +08:00
yusing
f79a15bac6 update license 2025-05-01 07:29:48 +08:00
yusing
2b4a70a550 fix(docker): fixed retry mechanism 2025-05-01 06:48:38 +08:00
yusing
f06741428c fix(idlewatcher): log error and retry instead instead of stopping 2025-05-01 06:46:24 +08:00
yusing
16e6e72454 feat(access_log): dynamic buffer size 2025-05-01 05:57:02 +08:00
yusing
100d2c392f chore: memory optimization for access log 2025-04-30 18:30:46 +08:00
yusing
829eb08e37 feat: tunable rotate interval 2025-04-30 18:19:00 +08:00
yusing
53d54a09b0 fix: rotate result file size, add "saved" and omit empty values 2025-04-30 18:17:09 +08:00
yusing
62c551c7fe fix: tests 2025-04-30 17:42:51 +08:00
yusing
80e59bb481 fix: nil panic on unmarshaling zero value 2025-04-30 12:06:49 +08:00
yusing
7a5afc3612 fix; compose example 2025-04-30 04:03:11 +08:00
yusing
2c0349c11c chore: remove debug statement 2025-04-30 00:14:53 +08:00
yusing
8e3c2cc8d4 fix: issues when using socket-proxy 2025-04-29 23:56:15 +08:00
yusing
d35afdb3c9 security: exclude socket-proxy from proxying 2025-04-29 16:23:30 +08:00
yusing
ae093ebf40 docs: update wiki URL, add website URL 2025-04-29 15:22:31 +08:00
yusing
aa8af4185b chore: update schema url 2025-04-29 14:45:38 +08:00
yusing
0029cf69d6 fix: setup script and compose 2025-04-29 09:24:22 +08:00
Yuzerion
33e400a17e
security: run in rootless by default and drop unnecessary caps (#101)
Co-authored-by: yusing <yusing@6uo.me>
2025-04-29 08:42:30 +08:00
yusing
1d22bcfed9 fix(access_log): file size calculation 2025-04-29 07:33:51 +08:00
yusing
978d82060e docs: move schema to frontend 2025-04-29 07:26:14 +08:00
yusing
7aa1215491 refactor: rename Deserialize to MapUnmarshalValidate 2025-04-29 07:26:14 +08:00
yusing
0b69589586 chore: disable unused last version parsing 2025-04-29 00:47:13 +08:00
yusing
bca3cd84d1 fix(accesslog): os: invalid use of WriteAt on file opened with O_APPEND 2025-04-29 00:46:30 +08:00
yusing
ce4bf2f646 fix(idlewatcher): not started for docker containers 2025-04-28 23:54:13 +08:00
yusing
c49016f22c fix: go.mod and deps upgrade 2025-04-28 11:32:01 +08:00
yusing
8da63daf02 refactor: simplify and remove duplicated code for icon caching 2025-04-28 11:22:49 +08:00
yusing
c5fd21552e fix(oidc): token not being refreshed when receiving simutaneous requests from the same session 2025-04-28 11:19:57 +08:00
yusing
27409abc24 fix: missing proxmox initialization 2025-04-28 05:08:14 +08:00
yusing
21c9e46274 fix: remove redundant event logging 2025-04-28 05:03:17 +08:00
yusing
22a12d3116 chore: remove redundant loadbalancer debug message 2025-04-28 04:57:26 +08:00
yusing
89d93dd878 chore: better error message 2025-04-28 00:48:20 +08:00
yusing
66853dfc52 fix: cloudflare realIP should defaults to be recursive 2025-04-27 23:53:04 +08:00
yusing
c72f66d64b feat(acl): add FORCE_RESOLVE_COUNTRY option to resolve country 2025-04-26 09:48:43 +08:00
yusing
59bc342a40 fix: notfications not being sent 2025-04-26 09:20:03 +08:00
yusing
e11579df10 chore(maxm): improved database update mechanism, fixed db being downloaded twice on first run 2025-04-26 09:08:03 +08:00
yusing
6a8f6fb4b5 chore(accesslog): reduce buffering for stdout 2025-04-26 08:29:55 +08:00
yusing
8f20bd3840 fix(acl): caching logic 2025-04-26 08:05:26 +08:00
yusing
f1abb745fe fix(tcp): return a dummy connection instead of nil 2025-04-26 07:57:20 +08:00
yusing
cb2990f6e8 chore: enrich example config 2025-04-26 07:40:55 +08:00
yusing
fb2f850311 fix(oidc): incorrect redirect url 2025-04-26 06:57:02 +08:00
288 changed files with 7825 additions and 6612 deletions

View file

@ -1,6 +1,13 @@
# docker image tag (latest, nightly)
TAG=latest
# set timezone to get correct log timestamp
TZ=ETC/UTC
# container uid and gid (must match the owner of mounted directories)
GODOXY_UID=1000
GODOXY_GID=1000
# API JWT Configuration (common)
# generate secret with `openssl rand -base64 32`
GODOXY_API_JWT_SECRET=
@ -41,14 +48,29 @@ GODOXY_API_PASSWORD=password
GODOXY_HTTP_ADDR=:80
GODOXY_HTTPS_ADDR=:443
# Enable HTTP3
GODOXY_HTTP3_ENABLED=true
# API listening address
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
GODOXY_FRONTEND_PORT=3000
# Prometheus Metrics
GODOXY_PROMETHEUS_ENABLED=true
# Frontend aliases (subdomains / FQDNs, e.g. godoxy, godoxy.domain.com)
GODOXY_FRONTEND_ALIASES=godoxy
# Docker socket
# /var/run/podman/podman.sock for podman
DOCKER_SOCKET=/var/run/docker.sock
SOCKET_PROXY_LISTEN_ADDR=127.0.0.1:2375
# Debug mode
GODOXY_DEBUG=false

View file

@ -15,9 +15,10 @@ jobs:
with:
image_name: ${{ github.repository_owner }}/godoxy
tag: nightly
target: main
build-nightly-agent:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/godoxy-agent
tag: nightly
agent: true
target: agent

View file

@ -12,9 +12,10 @@ jobs:
image_name: ${{ github.repository_owner }}/godoxy
old_image_name: ${{ github.repository_owner }}/go-proxy
tag: latest
target: main
build-prod-agent:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/godoxy-agent
tag: latest
agent: true
target: agent

View file

@ -0,0 +1,23 @@
name: Docker Image CI (socket-proxy)
on:
push:
branches:
- main
paths:
- "socket-proxy/**"
tags-ignore:
- '**'
workflow_dispatch:
permissions:
contents: read
jobs:
build:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/socket-proxy
tag: latest
target: socket-proxy
dockerfile: socket-proxy.Dockerfile

View file

@ -12,16 +12,20 @@ on:
old_image_name:
required: false
type: string
agent:
target:
required: true
type: string
dockerfile:
required: false
default: false
type: boolean
type: string
default: Dockerfile
env:
REGISTRY: ghcr.io
MAKE_ARGS: agent=${{ inputs.agent && '1' || '0' }}
DIGEST_PATH: /tmp/digests/${{ inputs.agent && 'agent' || 'main' }}
DIGEST_NAME_SUFFIX: ${{ inputs.agent && 'agent' || 'main' }}
MAKE_ARGS: ${{ inputs.target }}=1
DIGEST_PATH: /tmp/digests/${{ inputs.target }}
DIGEST_NAME_SUFFIX: ${{ inputs.target }}
DOCKERFILE: ${{ inputs.dockerfile }}
jobs:
build:
@ -76,11 +80,14 @@ jobs:
with:
platforms: ${{ matrix.platform }}
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
cache-from: |
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}-${{ inputs.tag }}
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}
type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }}
cache-to: |
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }}-${{ inputs.tag }},mode=max
type=registry,ref=${{ env.REGISTRY }}/${{ inputs.image_name }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max
type=gha,scope=${{ github.workflow }}-${{ env.PLATFORM_PAIR }},mode=max
build-args: |
VERSION=${{ github.ref_name }}
MAKE_ARGS=${{ env.MAKE_ARGS }}

View file

@ -1,43 +1,77 @@
run:
timeout: 10m
linters-settings:
govet:
enable-all: true
version: "2"
linters:
default: all
disable:
- shadow
- fieldalignment
gocyclo:
min-complexity: 14
misspell:
locale: US
- bodyclose
- containedctx
- contextcheck
- cyclop
- depguard
- dupl
- err113
- exhaustive
- exhaustruct
- forcetypeassert
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- gocyclo
- gomoddirectives
- gosec
- gosmopolitan
- ireturn
- lll
- maintidx
- makezero
- mnd
- nakedret
- nestif
- nilnil
- nlreturn
- noctx
- nonamedreturns
- paralleltest
- prealloc
- rowserrcheck
- sqlclosecheck
- tagliatelle
- testpackage
- tparallel
- varnamelen
- wrapcheck
- wsl
settings:
errcheck:
exclude-functions:
- fmt.Fprintln
forbidigo:
forbid:
- pattern: ^print(ln)?$
funlen:
lines: -1
statements: 120
forbidigo:
forbid:
- ^print(ln)?$
gocyclo:
min-complexity: 14
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
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
@ -67,69 +101,51 @@ linters-settings:
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
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
staticcheck:
checks:
- all
- -SA1019
errcheck:
exclude-functions:
- fmt.Fprintln
linters:
enable-all: true
disable:
- execinquery # deprecated
- gomnd # deprecated
- sqlclosecheck # not relevant (SQL)
- rowserrcheck # not relevant (SQL)
- cyclop # duplicate of gocyclo
- depguard # Not relevant
- nakedret # Too strict
- lll # Not relevant
- gocyclo # must be fixed
- gocognit # Too strict
- nestif # Too many false-positive.
- prealloc # Too many false-positive.
- makezero # Not relevant
- dupl # Too strict
- gci # I don't care
- goconst # Too annoying
- gosec # Too strict
- gochecknoinits
- gochecknoglobals
- wsl # Too strict
- nlreturn # Not relevant
- mnd # Too strict
- testpackage # Too strict
- tparallel # Not relevant
- paralleltest # Not relevant
- exhaustive # Not relevant
- exhaustruct # Not relevant
- err113 # Too strict
- wrapcheck # Too strict
- noctx # Too strict
- bodyclose # too many false-positive
- forcetypeassert # Too strict
- tagliatelle # Too strict
- varnamelen # Not relevant
- nilnil # Not relevant
- ireturn # Not relevant
- contextcheck # too many false-positive
- containedctx # too many false-positive
- maintidx # kind of duplicate of gocyclo
- nonamedreturns # Too strict
- gosmopolitan # not relevant
- exportloopref # Not relevant since go1.22
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$

View file

@ -2,36 +2,37 @@
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
version: 0.1
cli:
version: 1.22.10
version: 1.22.15
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
plugins:
sources:
- id: trunk
ref: v1.6.7
ref: v1.6.8
uri: https://github.com/trunk-io/plugins
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
runtimes:
enabled:
- node@18.20.5
- python@3.10.8
- go@1.23.2
- go@1.24.3
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
lint:
disabled:
- markdownlint
- yamllint
enabled:
- checkov@3.2.416
- golangci-lint2@2.1.6
- hadolint@2.12.1-beta
- actionlint@1.7.7
- git-diff-check
- gofmt@1.20.4
- golangci-lint@1.64.5
- osv-scanner@1.9.2
- oxipng@9.1.4
- prettier@3.5.1
- osv-scanner@2.0.2
- oxipng@9.1.5
- prettier@3.5.3
- shellcheck@0.10.0
- shfmt@3.6.0
- trufflehog@3.88.9
- trufflehog@3.88.29
actions:
disabled:
- trunk-announce

View file

@ -1,10 +1,10 @@
{
"yaml.schemas": {
"https://github.com/yusing/go-proxy/raw/main/schemas/config.schema.json": [
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/config.schema.json": [
"config.example.yml",
"config.yml"
],
"https://github.com/yusing/go-proxy/raw/main/schemas/routes.schema.json": [
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/routes.schema.json": [
"providers.example.yml"
]
}

View file

@ -1,27 +1,33 @@
# Stage 1: deps
FROM golang:1.24.2-alpine AS deps
FROM golang:1.24.3-alpine AS deps
HEALTHCHECK NONE
# package version does not matter
# trunk-ignore(hadolint/DL3018)
RUN apk add --no-cache tzdata make libcap-setcap
ENV GOPATH=/root/go
WORKDIR /src
COPY go.mod go.sum ./
# remove godoxy stuff from go.mod first
RUN sed -i '/^module github\.com\/yusing\/go-proxy/!{/github\.com\/yusing\/go-proxy/d}' go.mod && \
go mod download -x
# Stage 2: builder
FROM deps AS builder
WORKDIR /src
COPY go.mod go.sum ./
COPY Makefile ./
COPY cmd ./cmd
COPY internal ./internal
COPY pkg ./pkg
COPY agent ./agent
# Only copy go.mod and go.sum initially for better caching
COPY go.mod go.sum /src/
ENV GOPATH=/root/go
RUN go mod download -x
COPY socket-proxy ./socket-proxy
ARG VERSION
ENV VERSION=${VERSION}
@ -31,9 +37,10 @@ ENV MAKE_ARGS=${MAKE_ARGS}
ENV GOCACHE=/root/.cache/go-build
ENV GOPATH=/root/go
RUN make ${MAKE_ARGS} build link-binary && \
mv bin /app/ && \
mkdir -p /app/error_pages /app/certs
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/go/pkg/mod \
make ${MAKE_ARGS} docker=1 build
# Stage 3: Final image
FROM scratch
@ -45,10 +52,7 @@ LABEL proxy.exclude=1
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# copy binary
COPY --from=builder /app /app
# copy example config
COPY config.example.yml /app/config/config.yml
COPY --from=builder /app/run /app/run
# copy certs
COPY --from=builder /etc/ssl/certs /etc/ssl/certs

26
LICENSE
View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 [fullname]
Copyright (c) 2024 - present Yusing
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -19,3 +19,27 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
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.

View file

@ -1,3 +1,4 @@
shell := /bin/sh
export VERSION ?= $(shell git describe --tags --abbrev=0)
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
export GOOS = linux
@ -7,11 +8,12 @@ LDFLAGS = -X github.com/yusing/go-proxy/pkg.version=${VERSION}
ifeq ($(agent), 1)
NAME = godoxy-agent
CMD_PATH = ./cmd
PWD = ${shell pwd}/agent
else ifeq ($(socket-proxy), 1)
NAME = godoxy-socket-proxy
PWD = ${shell pwd}/socket-proxy
else
NAME = godoxy
CMD_PATH = ./cmd
PWD = ${shell pwd}
endif
@ -45,7 +47,6 @@ BUILD_FLAGS += -ldflags='$(LDFLAGS)'
BIN_PATH := $(shell pwd)/bin/${NAME}
export NAME
export CMD_PATH
export CGO_ENABLED
export GODOXY_DEBUG
export GODOXY_TRACE
@ -59,30 +60,63 @@ else
SETCAP_CMD = sudo setcap
endif
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
POST_BUILD = $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH};
ifeq ($(docker), 1)
POST_BUILD += mkdir -p /app && mv ${BIN_PATH} /app/run;
endif
.PHONY: debug
test:
GODOXY_TEST=1 go test ./internal/...
get:
for dir in ${PWD} ${PWD}/agent; do cd $$dir && go get -u ./... && go mod tidy; done
docker-build-test:
docker build -t godoxy .
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
gomod_paths := $(shell find . -name go.mod -type f | xargs dirname)
update-go:
for file in ${files}; do \
echo "updating $$file"; \
sed -i 's|go \([0-9]\+\.[0-9]\+\.[0-9]\+\)|go ${go_ver}|g' $$file; \
sed -i 's|FROM golang:.*-alpine|FROM golang:${go_ver}-alpine|g' $$file; \
done
for path in ${gomod_paths}; do \
echo "go mod tidy $$path"; \
cd ${PWD}/$$path && go mod tidy; \
done
update-deps:
for path in ${gomod_paths}; do \
echo "go get -u $$path"; \
cd ${PWD}/$$path && go get -u ./... && go mod tidy; \
done
mod-tidy:
for path in ${gomod_paths}; do \
echo "go mod tidy $$path"; \
cd ${PWD}/$$path && go mod tidy; \
done
build:
mkdir -p bin
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ${CMD_PATH}
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
$(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH}
mkdir -p $(shell dirname ${BIN_PATH})
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
${POST_BUILD}
run:
[ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ${CMD_PATH}
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
debug:
make NAME="godoxy-test" debug=1 build
sh -c 'HTTP_ADDR=:81 HTTPS_ADDR=:8443 API_ADDR=:8899 DEBUG=1 bin/godoxy-test'
mtrace:
bin/godoxy debug-ls-mtrace > mtrace.json
${BIN_PATH} debug-ls-mtrace > mtrace.json
rapid-crash:
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
@ -97,10 +131,7 @@ ci-test:
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
cloc:
cloc --not-match-f '_test.go$$' cmd internal pkg
link-binary:
ln -s /app/${NAME} bin/run
cloc --include-lang=Go --not-match-f '_test.go$$' .
push-github:
git push origin $(shell git rev-parse --abbrev-ref HEAD)

View file

@ -2,17 +2,19 @@
# GoDoxy
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=yusing_godoxy)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
![GitHub last commit](https://img.shields.io/github/last-commit/yusing/godoxy)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=yusing_godoxy)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=go-proxy)
![Demo](https://img.shields.io/website?url=https%3A%2F%2Fdemo.godoxy.dev&label=Demo&link=https%3A%2F%2Fdemo.godoxy.dev)
[![Discord](https://dcbadge.limes.pink/api/server/umReR62nRd?style=flat)](https://discord.gg/umReR62nRd)
A lightweight, simple, and [performant](https://github.com/yusing/godoxy/wiki/Benchmarks) reverse proxy with WebUI.
A lightweight, simple, and performant reverse proxy with WebUI.
For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki)**
<h5>
<a href="https://docs.godoxy.dev">Website</a> | <a href="https://docs.godoxy.dev/Home.html">Wiki</a> | <a href="https://discord.gg/umReR62nRd">Discord</a>
</h5>
**EN** | <a href="README_CHT.md">中文</a>
<h5>EN | <a href="README_CHT.md">中文</a></h5>
<img src="screenshots/webui.jpg" style="max-width: 650">
@ -45,8 +47,8 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
## Key Features
- **Simple**
- Effortless configuration with [simple labels](https://github.com/yusing/godoxy/wiki/Docker-labels-and-Route-Files) or WebUI
- [Simple multi-node setup](https://github.com/yusing/godoxy/wiki/Configurations#multi-docker-nodes-setup)
- Effortless configuration with [simple labels](https://docs.godoxy.dev/Docker-labels-and-Route-Files) or WebUI
- [Simple multi-node setup](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
- Detailed error messages for easy troubleshooting.
- **ACL**: connection / request level access control
- IP/CIDR
@ -54,7 +56,7 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
- Timezone **(Maxmind account required)**
- **Access logging**
- **Advanced Automation**
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://docs.godoxy.dev/DNS-01-Providers))
- Auto-configuration for Docker containers
- Hot-reloading of configurations and container state changes
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
@ -65,8 +67,8 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
- TCP/UDP port forwarding
- **OpenID Connect support**: SSO and secure your apps easily
- **Customization**
- [HTTP middlewares](https://github.com/yusing/go-proxy/wiki/Middlewares)
- [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
- [HTTP middlewares](https://docs.godoxy.dev/Middlewares)
- [Custom error pages support](https://docs.godoxy.dev/Custom-Error-Pages)
- **Web UI**
- App Dashboard
- Config Editor
@ -99,7 +101,13 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
```
3. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
3. Start the docker compose service from generated `compose.yml`:
```shell
docker compose up -d
```
4. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
## How does GoDoxy work

View file

@ -8,11 +8,13 @@
![Demo](https://img.shields.io/website?url=https%3A%2F%2Fdemo.godoxy.dev&label=Demo&link=https%3A%2F%2Fdemo.godoxy.dev)
[![Discord](https://dcbadge.limes.pink/api/server/umReR62nRd?style=flat)](https://discord.gg/umReR62nRd)
輕量、易用、 [高效能](https://github.com/yusing/godoxy/wiki/Benchmarks),且帶有主頁和配置面板的反向代理
輕量、易用、 高效能,且帶有主頁和配置面板的反向代理
完整文檔請查閱 **[Wiki](https://github.com/yusing/godoxy/wiki)**(暫未有中文翻譯)
<h5>
<a href="https://docs.godoxy.dev">網站</a> | <a href="https://docs.godoxy.dev/Home.html">文檔</a> | <a href="https://discord.gg/umReR62nRd">Discord</a>
</h5>
<a href="README.md">EN</a> | **中文**
<h5><a href="README.md">EN</a> | 中文</h5>
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
@ -43,20 +45,38 @@
## 主要特點
- 容易使用
- 輕鬆配置
- 簡單的多節點設置
- 錯誤訊息清晰詳細,易於排除故障
- 自動 SSL 憑證管理(參見 [支援的 DNS-01 驗證提供商](https://github.com/yusing/godoxy/wiki/Supported-DNS%E2%80%9001-Providers)
- 自動配置 Docker 容器
- 容器狀態/配置文件變更時自動熱重載
- **閒置休眠**在閒置時停止容器有流量時喚醒_可選參見[截圖](#閒置休眠)_
- OpenID Connect輕鬆實現單點登入
- HTTP(s) 反向代理和 TCP 和 UDP 埠轉發
- [HTTP 中介軟體](https://github.com/yusing/godoxy/wiki/Middlewares) 和 [自定義錯誤頁面](https://github.com/yusing/godoxy/wiki/Middlewares#custom-error-pages)
- **網頁介面,具有應用儀表板和配置編輯器**
- 支援 linux/amd64、linux/arm64
- 使用 **[Go](https://go.dev)** 編寫
- **簡單易用**
- 透過 Docker[標籤](https://docs.godoxy.dev/Docker-labels-and-Route-Files)或 WebUI 輕鬆設定
- [簡單的多節點設置](https://docs.godoxy.dev/Configurations#multi-docker-nodes-setup)
- 詳細的錯誤訊息,便於故障排除
- **存取控制 (ACL)**:連線/請求層級存取控制
- IP/CIDR
- 國家 **(需要 Maxmind 帳戶)**
- 時區 **(需要 Maxmind 帳戶)**
- **存取日誌記錄**
- **自動化**
- 使用 Let's Encrypt 自動管理 SSL 憑證 ([使用 DNS-01 驗證](https://docs.godoxy.dev/DNS-01-Providers))
- Docker 容器自動配置
- 設定檔與容器狀態變更時自動熱重載
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
- Docker 容器
- Proxmox LXC 容器
- **流量管理**
- HTTP 反向代理
- TCP/UDP 連接埠轉送
- **OpenID Connect 支援**:輕鬆實現單點登入 (SSO) 並保護您的應用程式
- **客製化**
- [HTTP 中介軟體](https://docs.godoxy.dev/Middlewares)
- [支援自訂錯誤頁面](https://docs.godoxy.dev/Custom-Error-Pages)
- **網頁使用者介面 (Web UI)**
- 應用程式一覽
- 設定編輯器
- 執行時間與系統指標
- Docker 日誌檢視器
- **跨平台支援**
- 支援 **linux/amd64** 與 **linux/arm64**
- **高效能**
- 以 **[Go](https://go.dev)** 語言編寫
[🔼 回到頂部](#目錄)

View file

@ -1,22 +1,19 @@
package main
import (
"os"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/env"
"github.com/yusing/go-proxy/agent/pkg/server"
"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"
httpServer "github.com/yusing/go-proxy/internal/net/gphttp/server"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg"
socketproxy "github.com/yusing/go-proxy/socketproxy/pkg"
)
func main() {
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
ca := &agent.PEMPair{}
err := ca.Load(env.AgentCACert)
if err != nil {
@ -55,6 +52,17 @@ Tips:
}
server.StartAgentServer(t, opts)
if socketproxy.ListenAddr != "" {
logging.Info().Msgf("Docker socket listening on: %s", socketproxy.ListenAddr)
opts := httpServer.Options{
Name: "docker",
HTTPAddr: socketproxy.ListenAddr,
Handler: socketproxy.NewHandler(),
}
httpServer.StartServer(t, opts)
}
systeminfo.Poller.Start()
task.WaitExit(3)

View file

@ -1,33 +1,38 @@
module github.com/yusing/go-proxy/agent
go 1.24.2
go 1.24.3
replace github.com/yusing/go-proxy => ..
replace github.com/yusing/go-proxy/socketproxy => ../socket-proxy
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250502022742-408a348f1b97
require (
github.com/coder/websocket v1.8.13
github.com/docker/docker v28.1.1+incompatible
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.10.0
github.com/yusing/go-proxy v0.11.1
github.com/yusing/go-proxy v0.0.0-00010101000000-000000000000
github.com/yusing/go-proxy/socketproxy v0.0.0-00010101000000-000000000000
)
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250418000134-7af8fd7b079e
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/diskfs/go-diskfs v1.6.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/docker/cli v28.1.1+incompatible // indirect
github.com/docker/docker v28.1.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/ebitengine/purego v0.8.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/go-acme/lego/v4 v4.23.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
@ -36,18 +41,21 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-yaml v1.17.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
github.com/gotify/server/v2 v2.6.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/gotify/server/v2 v2.6.3 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/luthermonson/go-proxmox v0.2.2 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.65 // indirect
github.com/miekg/dns v1.1.66 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
@ -56,33 +64,29 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/puzpuzpuz/xsync/v4 v4.1.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.51.0 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/samber/lo v1.50.0 // indirect
github.com/samber/slog-common v0.18.1 // indirect
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
github.com/shirou/gopsutil/v4 v4.25.3 // indirect
github.com/shirou/gopsutil/v4 v4.25.4 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/vincent-petithory/dataurl v1.0.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.1 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
go.uber.org/mock v0.5.2 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.32.0 // indirect
golang.org/x/tools v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -8,22 +8,12 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -41,10 +31,10 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/go-acme/lego/v4 v4.23.1 h1:lZ5fGtGESA2L9FB8dNTvrQUq3/X4QOb8ExkKyY7LSV4=
@ -68,38 +58,42 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godoxy-app/docker v0.0.0-20250418000134-7af8fd7b079e h1:LEbMtJ6loEubxetD+Aw8+1x0rShor5iMoy9WuFQ8hN8=
github.com/godoxy-app/docker v0.0.0-20250418000134-7af8fd7b079e/go.mod h1:3tMTnTkH7IN5smn7PX83XdmRnNj4Nw2/Pt8GgReqnKM=
github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1 h1:fsSqE28vU0PRkq9FdekirRoDBeYJ+UaJ9dTErdXflWg=
github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1/go.mod h1:av6ggKWQz6SEkFyShjDEgVqiIB0RHvEQNIkPeqgJEeE=
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250502022742-408a348f1b97 h1:i52gBYamrKs4DHT1+SiobW2im5UgTMVXK1KIL1djSeA=
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250502022742-408a348f1b97/go.mod h1:XvbfPmmrdpLrsKwj3irYkxt5ygyMcDsTQTJ7cnZ9RNQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.6.1 h1:Kf7v5fzBxzELzZa/jonWfwJMkqYqh1LBzBpCmt5QIAI=
github.com/gotify/server/v2 v2.6.1/go.mod h1:Dk8HLyTVDqmXM8YEg6tjROBen6mxyHZFRggJFHTwZLc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I=
github.com/gotify/server/v2 v2.6.3/go.mod h1:IyeQ/iL3vetcuqUAzkCMVObIMGGJx4zb13/mVatIwE8=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@ -119,8 +113,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
@ -143,8 +137,12 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -152,8 +150,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
@ -161,33 +159,26 @@ github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP
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.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ=
github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk=
github.com/samber/slog-zerolog/v2 v2.7.3 h1:/MkPDl/tJhijN2GvB1MWwBn2FU8RiL3rQ8gpXkQm2EY=
github.com/samber/slog-zerolog/v2 v2.7.3/go.mod h1:oWU7WHof4Xp8VguiNO02r1a4VzkgoOyOZhY5CuRke60=
github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE=
github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -201,24 +192,22 @@ go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -227,8 +216,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -250,10 +239,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -263,16 +250,18 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -284,8 +273,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -304,8 +293,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -316,19 +305,19 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU=
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f h1:tjZsroqekhC63+WMqzmWyW5Twj/ZfR5HAlpd5YQ1Vs0=
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197 h1:29cjnHVylHwTzH66WfFZqgSQgnxzvWE+jvBwpZCLRxY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -339,4 +328,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View file

@ -10,7 +10,7 @@ var (
AGENT_PORT="{{.Port}}" \
AGENT_CA_CERT="{{.CACert}}" \
AGENT_SSL_CERT="{{.SSLCert}}" \
bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/go-proxy/main/scripts/install-agent.sh)"`
bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/install-agent.sh)"`
installScriptTemplate = template.Must(template.New("install.sh").Parse(installScript))
)

View file

@ -5,6 +5,8 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/url"
@ -14,10 +16,7 @@ import (
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/agent/pkg/certs"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging"
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg"
)
@ -27,6 +26,7 @@ type AgentConfig struct {
httpClient *http.Client
tlsConfig *tls.Config
name string
version string
l zerolog.Logger
}
@ -80,7 +80,9 @@ func (cfg *AgentConfig) Parse(addr string) error {
return nil
}
func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte) error {
var serverVersion = pkg.GetVersion()
func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte) error {
clientCert, err := tls.X509KeyPair(crt, key)
if err != nil {
return err
@ -90,7 +92,7 @@ func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte)
caCertPool := x509.NewCertPool()
ok := caCertPool.AppendCertsFromPEM(ca)
if !ok {
return gperr.New("invalid ca certificate")
return errors.New("invalid ca certificate")
}
cfg.tlsConfig = &tls.Config{
@ -102,7 +104,7 @@ func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte)
// create transport and http client
cfg.httpClient = cfg.NewHTTPClient()
ctx, cancel := context.WithTimeout(parent.Context(), 5*time.Second)
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// get agent name
@ -121,33 +123,34 @@ func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte)
return err
}
agentVersion := string(agentVersionBytes)
cfg.version = string(agentVersionBytes)
agentVersion := pkg.ParseVersion(cfg.version)
if pkg.GetVersion().IsNewerMajorThan(pkg.ParseVersion(agentVersion)) {
logging.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.name, pkg.GetVersion(), agentVersion)
if serverVersion.IsNewerMajorThan(agentVersion) {
logging.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.name, serverVersion, agentVersion)
}
logging.Info().Msgf("agent %q initialized", cfg.name)
return nil
}
func (cfg *AgentConfig) Start(parent task.Parent) gperr.Error {
func (cfg *AgentConfig) Start(ctx context.Context) error {
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
if !ok {
return gperr.New("invalid agent host").Subject(cfg.Addr)
return fmt.Errorf("invalid agent host: %s", cfg.Addr)
}
certData, err := os.ReadFile(filepath)
if err != nil {
return gperr.Wrap(err, "failed to read agent certs")
return fmt.Errorf("failed to read agent certs: %w", err)
}
ca, crt, key, err := certs.ExtractCert(certData)
if err != nil {
return gperr.Wrap(err, "failed to extract agent certs")
return fmt.Errorf("failed to extract agent certs: %w", err)
}
return gperr.Wrap(cfg.StartWithCerts(parent, ca, crt, key))
return cfg.StartWithCerts(ctx, ca, crt, key)
}
func (cfg *AgentConfig) NewHTTPClient() *http.Client {
@ -171,8 +174,10 @@ func (cfg *AgentConfig) Transport() *http.Transport {
}
}
var dialer = &net.Dialer{Timeout: 5 * time.Second}
func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
return gphttp.DefaultDialer.DialContext(ctx, "tcp", cfg.Addr)
return dialer.DialContext(ctx, "tcp", cfg.Addr)
}
func (cfg *AgentConfig) Name() string {
@ -187,5 +192,6 @@ func (cfg *AgentConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"name": cfg.Name(),
"addr": cfg.Addr,
"version": cfg.version,
})
}

View file

@ -2,7 +2,6 @@ package agent
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
@ -12,20 +11,37 @@ import (
"math/big"
"strings"
"time"
"crypto/ecdsa"
"crypto/elliptic"
"fmt"
)
const (
CertsDNSName = "godoxy.agent"
KeySize = 2048
)
func toPEMPair(certDER []byte, key *rsa.PrivateKey) *PEMPair {
func toPEMPair(certDER []byte, key *ecdsa.PrivateKey) *PEMPair {
marshaledKey, err := marshalECPrivateKey(key)
if err != nil {
// This is a critical internal error during PEM encoding of a newly generated key.
// Panicking is acceptable here as it indicates a fundamental issue.
panic(fmt.Sprintf("failed to marshal EC private key for PEM encoding: %v", err))
}
return &PEMPair{
Cert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}),
Key: pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}),
Key: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: marshaledKey}),
}
}
func marshalECPrivateKey(key *ecdsa.PrivateKey) ([]byte, error) {
derBytes, err := x509.MarshalECPrivateKey(key)
if err != nil {
return nil, fmt.Errorf("failed to marshal EC private key: %w", err)
}
return derBytes, nil
}
func b64Encode(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
@ -63,10 +79,23 @@ func (p *PEMPair) ToTLSCert() (*tls.Certificate, error) {
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) {
caSerialNumber, err := newSerialNumber()
if err != nil {
return nil, nil, nil, err
}
// Create the CA's certificate
caTemplate := &x509.Certificate{
SerialNumber: big.NewInt(1),
SerialNumber: caSerialNumber,
Subject: pkix.Name{
Organization: []string{"GoDoxy"},
CommonName: CertsDNSName,
@ -76,9 +105,12 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 0,
MaxPathLenZero: true,
SignatureAlgorithm: x509.ECDSAWithSHA256,
}
caKey, err := rsa.GenerateKey(rand.Reader, KeySize)
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, nil, err
}
@ -91,20 +123,29 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
ca = toPEMPair(caDER, caKey)
// Generate a new private key for the server certificate
serverKey, err := rsa.GenerateKey(rand.Reader, KeySize)
serverKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, nil, err
}
serverSerialNumber, err := newSerialNumber()
if err != nil {
return nil, nil, nil, err
}
srvTemplate := &x509.Certificate{
SerialNumber: big.NewInt(2),
SerialNumber: serverSerialNumber,
Issuer: caTemplate.Subject,
Subject: caTemplate.Subject,
Subject: pkix.Name{
Organization: caTemplate.Subject.Organization,
OrganizationalUnit: []string{"Server"},
CommonName: CertsDNSName,
},
DNSNames: []string{CertsDNSName},
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)
@ -114,20 +155,29 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
srv = toPEMPair(srvCertDER, serverKey)
clientKey, err := rsa.GenerateKey(rand.Reader, KeySize)
clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, nil, err
}
clientSerialNumber, err := newSerialNumber()
if err != nil {
return nil, nil, nil, err
}
clientTemplate := &x509.Certificate{
SerialNumber: big.NewInt(3),
SerialNumber: clientSerialNumber,
Issuer: caTemplate.Subject,
Subject: caTemplate.Subject,
Subject: pkix.Name{
Organization: caTemplate.Subject.Organization,
OrganizationalUnit: []string{"Client"},
CommonName: CertsDNSName,
},
DNSNames: []string{CertsDNSName},
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)
if err != nil {

View file

@ -8,59 +8,59 @@ import (
"net/http/httptest"
"testing"
. "github.com/yusing/go-proxy/internal/utils/testing"
"github.com/stretchr/testify/require"
)
func TestNewAgent(t *testing.T) {
ca, srv, client, err := NewAgent()
ExpectNoError(t, err)
ExpectTrue(t, ca != nil)
ExpectTrue(t, srv != nil)
ExpectTrue(t, client != nil)
require.NoError(t, err)
require.NotNil(t, ca)
require.NotNil(t, srv)
require.NotNil(t, client)
}
func TestPEMPair(t *testing.T) {
ca, srv, client, err := NewAgent()
ExpectNoError(t, err)
require.NoError(t, err)
for i, p := range []*PEMPair{ca, srv, client} {
t.Run(fmt.Sprintf("load-%d", i), func(t *testing.T) {
var pp PEMPair
err := pp.Load(p.String())
ExpectNoError(t, err)
ExpectEqual(t, p.Cert, pp.Cert)
ExpectEqual(t, p.Key, pp.Key)
require.NoError(t, err)
require.Equal(t, p.Cert, pp.Cert)
require.Equal(t, p.Key, pp.Key)
})
}
}
func TestPEMPairToTLSCert(t *testing.T) {
ca, srv, client, err := NewAgent()
ExpectNoError(t, err)
require.NoError(t, err)
for i, p := range []*PEMPair{ca, srv, client} {
t.Run(fmt.Sprintf("toTLSCert-%d", i), func(t *testing.T) {
cert, err := p.ToTLSCert()
ExpectNoError(t, err)
ExpectTrue(t, cert != nil)
require.NoError(t, err)
require.NotNil(t, cert)
})
}
}
func TestServerClient(t *testing.T) {
ca, srv, client, err := NewAgent()
ExpectNoError(t, err)
require.NoError(t, err)
srvTLS, err := srv.ToTLSCert()
ExpectNoError(t, err)
ExpectTrue(t, srvTLS != nil)
require.NoError(t, err)
require.NotNil(t, srvTLS)
clientTLS, err := client.ToTLSCert()
ExpectNoError(t, err)
ExpectTrue(t, clientTLS != nil)
require.NoError(t, err)
require.NotNil(t, clientTLS)
caPool := x509.NewCertPool()
ExpectTrue(t, caPool.AppendCertsFromPEM(ca.Cert))
require.True(t, caPool.AppendCertsFromPEM(ca.Cert))
srvTLSConfig := &tls.Config{
Certificates: []tls.Certificate{*srvTLS},
@ -86,6 +86,6 @@ func TestServerClient(t *testing.T) {
}
resp, err := httpClient.Get(server.URL)
ExpectNoError(t, err)
ExpectEqual(t, resp.StatusCode, http.StatusOK)
require.NoError(t, err)
require.Equal(t, resp.StatusCode, http.StatusOK)
}

View file

@ -9,6 +9,36 @@ services:
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:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data

16
agent/pkg/env/env.go vendored
View file

@ -15,10 +15,24 @@ func DefaultAgentName() string {
}
var (
AgentName string
AgentPort int
AgentSkipClientCertCheck bool
AgentCACert string
AgentSSLCert string
DockerSocket string
)
func init() {
Load()
}
func Load() {
DockerSocket = common.GetEnvString("DOCKER_SOCKET", "/var/run/docker.sock")
AgentName = common.GetEnvString("AGENT_NAME", DefaultAgentName())
AgentPort = common.GetEnvInt("AGENT_PORT", 8890)
AgentSkipClientCertCheck = common.GetEnvBool("AGENT_SKIP_CLIENT_CERT_CHECK", false)
AgentCACert = common.GetEnvString("AGENT_CA_CERT", "")
AgentSSLCert = common.GetEnvString("AGENT_SSL_CERT", "")
)
}

View file

@ -1,13 +1,13 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/watcher/health"
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
)
@ -18,7 +18,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
scheme := query.Get("scheme")
if scheme == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
http.Error(w, "missing scheme", http.StatusBadRequest)
return
}
@ -28,7 +28,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
case "fileserver":
path := query.Get("path")
if path == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
http.Error(w, "missing path", http.StatusBadRequest)
return
}
_, err := os.Stat(path)
@ -40,7 +40,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
host := query.Get("host")
path := query.Get("path")
if host == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
http.Error(w, "missing host", http.StatusBadRequest)
return
}
result, err = monitor.NewHTTPHealthMonitor(&url.URL{
@ -51,17 +51,18 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
case "tcp", "udp":
host := query.Get("host")
if host == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
http.Error(w, "missing host", http.StatusBadRequest)
return
}
hasPort := strings.Contains(host, ":")
port := query.Get("port")
if port != "" && !hasPort {
host = fmt.Sprintf("%s:%s", host, port)
} else {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
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)
}
result, err = monitor.NewRawHealthMonitor(&url.URL{
Scheme: scheme,
Host: host,
@ -73,5 +74,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
return
}
gphttp.RespondJSON(w, r, result)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(result)
}

View file

@ -1,31 +0,0 @@
package handler
import (
"net/http"
"net/url"
"github.com/docker/docker/client"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/docker"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
"github.com/yusing/go-proxy/internal/net/types"
)
func serviceUnavailable(w http.ResponseWriter, r *http.Request) {
http.Error(w, "docker socket is not available", http.StatusServiceUnavailable)
}
func DockerSocketHandler() http.HandlerFunc {
dockerClient, err := docker.NewClient(common.DockerHostFromEnv)
if err != nil {
logging.Warn().Err(err).Msg("failed to connect to docker client")
return serviceUnavailable
}
rp := reverseproxy.NewReverseProxy("docker", types.NewURL(&url.URL{
Scheme: "http",
Host: client.DummyHost,
}), dockerClient.HTTPClient().Transport)
return rp.ServeHTTP
}

View file

@ -1,49 +1,57 @@
package handler
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"time"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/env"
"github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
"github.com/yusing/go-proxy/internal/utils/strutils"
"github.com/yusing/go-proxy/pkg"
)
type ServeMux struct{ *http.ServeMux }
func (mux ServeMux) HandleMethods(methods, endpoint string, handler http.HandlerFunc) {
for _, m := range strutils.CommaSeperatedList(methods) {
mux.ServeMux.HandleFunc(m+" "+agent.APIEndpointBase+endpoint, handler)
}
func (mux ServeMux) HandleEndpoint(method, endpoint string, handler http.HandlerFunc) {
mux.ServeMux.HandleFunc(method+" "+agent.APIEndpointBase+endpoint, handler)
}
func (mux ServeMux) HandleFunc(endpoint string, handler http.HandlerFunc) {
mux.ServeMux.HandleFunc(agent.APIEndpointBase+endpoint, handler)
}
type NopWriteCloser struct {
io.Writer
var dialer = &net.Dialer{KeepAlive: 1 * time.Second}
func dialDockerSocket(ctx context.Context, _, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, "unix", env.DockerSocket)
}
func (NopWriteCloser) Close() error {
return nil
func dockerSocketHandler() http.HandlerFunc {
rp := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: "api.moby.localhost",
})
rp.Transport = &http.Transport{
DialContext: dialDockerSocket,
}
return rp.ServeHTTP
}
func NewAgentHandler() http.Handler {
mux := ServeMux{http.NewServeMux()}
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
mux.HandleMethods("GET", agent.EndpointVersion, pkg.GetVersionHTTPHandler())
mux.HandleMethods("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
mux.HandleEndpoint("GET", agent.EndpointVersion, pkg.GetVersionHTTPHandler())
mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, env.AgentName)
})
mux.HandleMethods("GET", agent.EndpointHealth, CheckHealth)
mux.HandleMethods("GET", agent.EndpointLogs, memlogger.HandlerFunc())
mux.HandleMethods("GET", agent.EndpointSystemInfo, systeminfo.Poller.ServeHTTP)
mux.ServeMux.HandleFunc("/", DockerSocketHandler())
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, systeminfo.Poller.ServeHTTP)
mux.ServeMux.HandleFunc("/", dockerSocketHandler())
return mux
}

View file

@ -3,18 +3,26 @@ package handler
import (
"crypto/tls"
"net/http"
"net/url"
"net/http/httputil"
"strconv"
"time"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/agentproxy"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
"github.com/yusing/go-proxy/internal/net/types"
)
func NewTransport() *http.Transport {
return &http.Transport{
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ResponseHeaderTimeout: 60 * time.Second,
WriteBufferSize: 16 * 1024, // 16KB
ReadBufferSize: 16 * 1024, // 16KB
}
}
func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
host := r.Header.Get(agentproxy.HeaderXProxyHost)
isHTTPS, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
@ -34,11 +42,9 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
scheme = "https"
}
var transport *http.Transport
transport := NewTransport()
if skipTLSVerify {
transport = gphttp.NewTransportWithTLSConfig(&tls.Config{InsecureSkipVerify: true})
} else {
transport = gphttp.NewTransport()
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
if responseHeaderTimeout > 0 {
@ -49,14 +55,13 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
r.URL.Host = ""
r.URL.Path = r.URL.Path[agent.HTTPProxyURLPrefixLen:] // strip the {API_BASE}/proxy/http prefix
r.RequestURI = r.URL.String()
r.URL.Host = host
rp := &httputil.ReverseProxy{
Director: func(r *http.Request) {
r.URL.Scheme = scheme
logging.Debug().Msgf("proxy http request: %s %s", r.Method, r.URL.String())
rp := reverseproxy.NewReverseProxy("agent", types.NewURL(&url.URL{
Scheme: scheme,
Host: host,
}), transport)
r.URL.Host = host
},
Transport: transport,
}
rp.ServeHTTP(w, r)
}

View file

@ -1,30 +1,24 @@
package main
import (
"encoding/json"
"log"
"os"
"sync"
"github.com/yusing/go-proxy/internal"
"github.com/yusing/go-proxy/internal/api/v1/query"
"github.com/yusing/go-proxy/internal/auth"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config"
"github.com/yusing/go-proxy/internal/dnsproviders"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
"github.com/yusing/go-proxy/internal/metrics/uptime"
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
"github.com/yusing/go-proxy/internal/route/routes"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg"
)
var rawLogger = log.New(os.Stdout, "", 0)
func parallel(fns ...func()) {
var wg sync.WaitGroup
for _, fn := range fns {
@ -39,97 +33,29 @@ func parallel(fns ...func()) {
func main() {
initProfiling()
dnsproviders.InitProviders()
args := pkg.GetArgs(common.MainServerCommandValidator{})
switch args.Command {
case common.CommandReload:
if err := query.ReloadServer(); err != nil {
gperr.LogFatal("server reload error", err)
}
rawLogger.Println("ok")
return
case common.CommandListIcons:
icons, err := internal.ListAvailableIcons()
if err != nil {
rawLogger.Fatal(err)
}
printJSON(icons)
return
case common.CommandListRoutes:
routes, err := query.ListRoutes()
if err != nil {
log.Printf("failed to connect to api server: %s", err)
log.Printf("falling back to config file")
} else {
printJSON(routes)
return
}
case common.CommandDebugListMTrace:
trace, err := query.ListMiddlewareTraces()
if err != nil {
log.Fatal(err)
}
printJSON(trace)
return
}
if args.Command == common.CommandStart {
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
logging.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
logging.Trace().Msg("trace enabled")
parallel(
internal.InitIconListCache,
dnsproviders.InitProviders,
homepage.InitIconListCache,
systeminfo.Poller.Start,
middleware.LoadComposeFiles,
)
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 {
prepareDirectory(dir)
}
middleware.LoadComposeFiles()
var cfg *config.Config
var err gperr.Error
if cfg, err = config.Load(); err != nil {
cfg, err := config.Load()
if err != nil {
gperr.LogWarn("errors in config", err)
err = nil
}
switch args.Command {
case common.CommandListRoutes:
cfg.StartProxyProviders()
printJSON(routes.ByAlias())
return
case common.CommandListConfigs:
printJSON(cfg.Value())
return
case common.CommandDebugListEntries:
printJSON(cfg.DumpRoutes())
return
case common.CommandDebugListProviders:
printJSON(cfg.DumpRouteProviders())
return
}
cfg.Start(&config.StartServersOptions{
@ -156,11 +82,3 @@ func prepareDirectory(dir string) {
}
}
}
func printJSON(obj any) {
j, err := json.MarshalIndent(obj, "", " ")
if err != nil {
logging.Fatal().Err(err).Send()
}
rawLogger.Print(string(j)) // raw output for convenience using "jq"
}

View file

@ -1,21 +1,46 @@
---
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:
image: ghcr.io/yusing/godoxy-frontend:latest
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
container_name: godoxy-frontend
restart: unless-stopped
network_mode: host # do not change this
env_file: .env
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- all
depends_on:
- app
environment:
HOSTNAME: 127.0.0.1
PORT: ${GODOXY_FRONTEND_PORT:-3000}
# modify below to fit your needs
labels:
proxy.aliases: godoxy
proxy.godoxy.port: ${GODOXY_FRONTEND_PORT:-3000}
# proxy.godoxy.middlewares.cidr_whitelist: |
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
proxy.#1.port: ${GODOXY_FRONTEND_PORT:-3000}
# proxy.#1.middlewares.cidr_whitelist: |
# status: 403
# message: IP not allowed
# allow:
@ -24,16 +49,27 @@ services:
# - 192.168.0.0/16
# - 172.16.0.0/12
app:
image: ghcr.io/yusing/godoxy:latest
image: ghcr.io/yusing/godoxy:${TAG:-latest}
container_name: godoxy
restart: always
network_mode: host # do not change this
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:
- /var/run/docker.sock:/var/run/docker.sock
- ./config:/app/config
- ./logs:/app/logs
- ./error_pages:/app/error_pages
- ./error_pages:/app/error_pages:ro
- ./data:/app/data
# To use autocert, certs will be stored in "./certs".

View file

@ -38,10 +38,25 @@
entrypoint:
# Below define an example of middleware config
# 1. block non local IP connections
# 2. redirect HTTP to HTTPS
# 1. set security headers
# 2. block non local IP connections
# 3. redirect HTTP to HTTPS
#
# middlewares:
middlewares:
- use: CloudflareRealIP
- use: ModifyResponse
set_headers:
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
Access-Control-Allow-Headers: "*"
Access-Control-Allow-Origin: "*"
Access-Control-Max-Age: 180
Vary: "*"
X-XSS-Protection: 1; mode=block
Content-Security-Policy: "object-src 'self'; frame-ancestors 'self';"
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: same-origin
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# - use: CIDRWhitelist
# allow:
# - "127.0.0.1"

95
go.mod
View file

@ -1,11 +1,17 @@
module github.com/yusing/go-proxy
go 1.24.2
go 1.24.3
replace github.com/yusing/go-proxy/agent => ./agent
replace github.com/yusing/go-proxy/internal/dnsproviders => ./internal/dnsproviders
replace github.com/coreos/go-oidc/v3 => github.com/godoxy-app/go-oidc/v3 v3.14.2
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250502022742-408a348f1b97
require (
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
github.com/coder/websocket v1.8.13 // websocket for API and agent
@ -15,25 +21,21 @@ require (
github.com/go-acme/lego/v4 v4.23.1 // 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/gotify/server/v2 v2.6.1 // reference the Message struct for json response
github.com/gotify/server/v2 v2.6.3 // reference the Message struct for json response
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
github.com/puzpuzpuz/xsync/v3 v3.5.1 // lock free map for concurrent operations
github.com/puzpuzpuz/xsync/v4 v4.1.0 // lock free map for concurrent operations
github.com/rs/zerolog v1.34.0 // logging
github.com/shirou/gopsutil/v4 v4.25.3 // system info metrics
github.com/shirou/gopsutil/v4 v4.25.4 // system info metrics
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
golang.org/x/crypto v0.37.0 // encrypting password with bcrypt
golang.org/x/net v0.39.0 // HTTP header utilities
golang.org/x/oauth2 v0.29.0 // oauth2 authentication
golang.org/x/text v0.24.0 // string utilities
golang.org/x/crypto v0.38.0 // encrypting password with bcrypt
golang.org/x/net v0.40.0 // HTTP header utilities
golang.org/x/oauth2 v0.30.0 // oauth2 authentication
golang.org/x/time v0.11.0 // time utilities
gopkg.in/yaml.v3 v3.0.1 // indirect; yaml parsing for different config files
)
replace github.com/coreos/go-oidc/v3 => github.com/godoxy-app/go-oidc/v3 v3.14.2
require (
github.com/docker/cli v28.1.1+incompatible
github.com/goccy/go-yaml v1.17.1
github.com/goccy/go-yaml v1.17.1 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/luthermonson/go-proxmox v0.2.2
github.com/oschwald/maxminddb-golang v1.13.1
@ -46,21 +48,13 @@ require (
go.uber.org/atomic v1.11.0
)
require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
)
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1
require (
cloud.google.com/go/auth v0.16.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
@ -86,22 +80,22 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
github.com/aws/smithy-go v1.22.3 // indirect
github.com/baidubce/bce-sdk-go v0.9.224 // indirect
github.com/baidubce/bce-sdk-go v0.9.226 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/boombuler/barcode v1.0.2 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/civo/civogo v0.3.99 // indirect
github.com/civo/civogo v0.5.0 // indirect
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/diskfs/go-diskfs v1.6.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/exoscale/egoscale/v3 v3.1.15 // indirect
github.com/ebitengine/purego v0.8.3 // indirect
github.com/exoscale/egoscale/v3 v3.1.17 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
@ -120,18 +114,18 @@ require (
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/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/gophercloud/gophercloud v1.14.1 // indirect
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.146 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.149 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
@ -143,14 +137,14 @@ require (
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/linode/linodego v1.49.0 // indirect
github.com/linode/linodego v1.50.0 // indirect
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.65 // indirect
github.com/miekg/dns v1.1.66 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -173,7 +167,7 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/oracle/oci-go-sdk/v65 v65.89.2 // indirect
github.com/oracle/oci-go-sdk/v65 v65.91.0 // indirect
github.com/ovh/go-ovh v1.7.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
@ -188,10 +182,10 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sacloud/api-client-go v0.2.10 // indirect
github.com/sacloud/go-http v0.1.9 // indirect
github.com/sacloud/iaas-api-go v1.14.0 // indirect
github.com/sacloud/iaas-api-go v1.15.0 // indirect
github.com/sacloud/packages-go v0.0.11 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/samber/lo v1.50.0 // indirect
github.com/samber/slog-common v0.18.1 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 // indirect
github.com/selectel/domains-go v1.1.0 // indirect
@ -203,11 +197,11 @@ require (
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cast v1.8.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1151 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1164 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
@ -215,8 +209,8 @@ require (
github.com/transip/gotransip/v6 v6.26.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/volcengine/volc-sdk-golang v1.0.206 // indirect
github.com/vultr/govultr/v3 v3.19.1 // indirect
github.com/volcengine/volc-sdk-golang v1.0.207 // indirect
github.com/vultr/govultr/v3 v3.20.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
@ -224,29 +218,32 @@ require (
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.1 // indirect
go.uber.org/mock v0.5.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/ratelimit v0.3.1 // 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/api v0.230.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197 // indirect
google.golang.org/grpc v1.72.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0
golang.org/x/tools v0.33.0 // indirect
google.golang.org/api v0.233.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.14.2 // indirect
gopkg.in/ns1/ns1-go.v2 v2.14.3 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.33.0 // indirect
k8s.io/apimachinery v0.33.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect

167
go.sum
View file

@ -179,8 +179,8 @@ cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZ
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=
cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w=
@ -606,8 +606,8 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesn
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 h1:j8BorDEigD8UFOSZQiSqAMOOleyQOOQPnUAwV+Ls1gA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
@ -710,8 +710,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjK
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/baidubce/bce-sdk-go v0.9.224 h1:z2L8alGw/y3IUHjrLRyrxrgCvMssYTjgCd7OQdb4gt0=
github.com/baidubce/bce-sdk-go v0.9.224/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/baidubce/bce-sdk-go v0.9.226 h1:VKEKcJC9P33yIfYJZr12Q/4Bvj18RFbgO8w8XOfU8AI=
github.com/baidubce/bce-sdk-go v0.9.226/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@ -749,8 +749,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/civo/civogo v0.3.99 h1:ijv3ecaz9Ju82J72kbQwQvzlSxn9fLMlklu8C8pXGjI=
github.com/civo/civogo v0.3.99/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
github.com/civo/civogo v0.5.0 h1:YDG38z+hmgaAhpDad1n/9sZrBtwRTeKVvMgdrS8Gwy4=
github.com/civo/civogo v0.5.0/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM=
@ -811,8 +811,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
@ -831,8 +831,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/exoscale/egoscale/v3 v3.1.15 h1:L2p9jWZhOeSBEwXCP12LPAoclBUv4LqzSN4RgNJtMdg=
github.com/exoscale/egoscale/v3 v3.1.15/go.mod h1:t9+MpSEam94na48O/xgvvPFpQPRiwZ3kBN4/UuQtKco=
github.com/exoscale/egoscale/v3 v3.1.17 h1:+T6+GP/k3tFNsYIQzpF3Sou4ecH//FkERDsJze/OZ00=
github.com/exoscale/egoscale/v3 v3.1.17/go.mod h1:t9+MpSEam94na48O/xgvvPFpQPRiwZ3kBN4/UuQtKco=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@ -933,6 +933,8 @@ github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1 h1:fsSqE28vU0PRk
github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1/go.mod h1:av6ggKWQz6SEkFyShjDEgVqiIB0RHvEQNIkPeqgJEeE=
github.com/godoxy-app/go-oidc/v3 v3.14.2 h1:y1sosR6N7IpMiREM8I8w68zrUhh5P0Hg+6wERmuhFAc=
github.com/godoxy-app/go-oidc/v3 v3.14.2/go.mod h1:ZRZLrEz7MmMe1kRzRsYqYmWKN2EHlPVGn71GMbrLLt4=
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250502022742-408a348f1b97 h1:i52gBYamrKs4DHT1+SiobW2im5UgTMVXK1KIL1djSeA=
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250502022742-408a348f1b97/go.mod h1:XvbfPmmrdpLrsKwj3irYkxt5ygyMcDsTQTJ7cnZ9RNQ=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
@ -1033,8 +1035,8 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
@ -1063,8 +1065,8 @@ github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMd
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
@ -1082,16 +1084,17 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.6.1 h1:Kf7v5fzBxzELzZa/jonWfwJMkqYqh1LBzBpCmt5QIAI=
github.com/gotify/server/v2 v2.6.1/go.mod h1:Dk8HLyTVDqmXM8YEg6tjROBen6mxyHZFRggJFHTwZLc=
github.com/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I=
github.com/gotify/server/v2 v2.6.3/go.mod h1:IyeQ/iL3vetcuqUAzkCMVObIMGGJx4zb13/mVatIwE8=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
@ -1147,8 +1150,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.146 h1:ld5s5UeA9zgyFsZskVD2Tr6k6VnJWkvaLm5nqvfOEf4=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.146/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.149 h1:gDzo/eYE8/mwF5fi7v10pdBW2027PUvaHvRPA5aOPoM=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.149/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -1161,8 +1164,8 @@ github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 h1:AKsihjFT/t6Y0keEv3p59DACcOuh0inWXdUB0ZOzYH0=
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0/go.mod h1:NeNJpz09efw/edzqkVivGv1bWqBXTomqYBRFbP+XBqg=
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
@ -1232,8 +1235,8 @@ github.com/labbsr0x/goh v1.0.1 h1:97aBJkDjpyBZGPbQuOK5/gHcSFbcr5aRsq3RSRJFpPk=
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
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/linode/linodego v1.49.0 h1:MNd3qwvQzbXB5mCpvdCqlUIu1RPA9oC+50LyB9kK+GQ=
github.com/linode/linodego v1.49.0/go.mod h1:B+HAM3//4w1wOS0BwdaQBKwBxlfe6kYJ7bSC6jJ/xtc=
github.com/linode/linodego v1.50.0 h1:5y79VvvQnWb5JyPIjTwyUrU3ArHcs7XZQFdkPS/lNpw=
github.com/linode/linodego v1.50.0/go.mod h1:9S+REoPCtUNWCm63D1vjjxIJZfwEL2t2kTDnwt620FM=
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
@ -1287,8 +1290,8 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34=
github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
@ -1395,8 +1398,8 @@ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYr
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
github.com/oracle/oci-go-sdk/v65 v65.89.2 h1:w0GwID9NlT+eg3InbAwkWsazDtxVLYQ8rJb4E33Yb14=
github.com/oracle/oci-go-sdk/v65 v65.89.2/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
github.com/oracle/oci-go-sdk/v65 v65.91.0 h1:maO6AxKxVfszH0X4tbbtN21jOk03lCRR3IqiA8/FzZc=
github.com/oracle/oci-go-sdk/v65 v65.91.0/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
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/ovh/go-ovh v1.7.0 h1:V14nF7FwDjQrZt9g7jzcvAAQ3HN6DNShRFRMC3jLoPw=
@ -1477,16 +1480,16 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/regfish/regfish-dnsapi-go v0.1.1 h1:TJFtbePHkd47q5GZwYl1h3DIYXmoxdLjW/SBsPtB5IE=
github.com/regfish/regfish-dnsapi-go v0.1.1/go.mod h1:ubIgXSfqarSnl3XHSn8hIFwFF3h0yrq0ZiWD93Y2VjY=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@ -1510,15 +1513,15 @@ github.com/sacloud/api-client-go v0.2.10 h1:+rv3jDohD+pkdYwOTBiB+jZsM0xK3AxadXRz
github.com/sacloud/api-client-go v0.2.10/go.mod h1:Jj3CTy2+O4bcMedVDXlbHuqqche85HEPuVXoQFhLaRc=
github.com/sacloud/go-http v0.1.9 h1:Xa5PY8/pb7XWhwG9nAeXSrYXPbtfBWqawgzxD5co3VE=
github.com/sacloud/go-http v0.1.9/go.mod h1:DpDG+MSyxYaBwPJ7l3aKLMzwYdTVtC5Bo63HActcgoE=
github.com/sacloud/iaas-api-go v1.14.0 h1:xjkFWqdo4ilTrKPNNYBNWR/CZ/kVRsJrdAHAad6J/AQ=
github.com/sacloud/iaas-api-go v1.14.0/go.mod h1:C8os2Mnj0TOmMdSllwhaDWKMVG2ysFnpe69kyA4M3V0=
github.com/sacloud/iaas-api-go v1.15.0 h1:G88U69OOVUSjOZ2fEP5QWMg7r3VOVsfNkl5Mxl+/wMk=
github.com/sacloud/iaas-api-go v1.15.0/go.mod h1:AU6TM3giGEeyl/p1FAYfpwMDpkl7aLco2svdYXnrAfA=
github.com/sacloud/packages-go v0.0.11 h1:hrRWLmfPM9w7GBs6xb5/ue6pEMl8t1UuDKyR/KfteHo=
github.com/sacloud/packages-go v0.0.11/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZOmmQORIzcurJ8=
github.com/sagikazarmark/crypt v0.10.0/go.mod h1:gwTNHQVoOS3xp9Xvz5LLR+1AauC5M6880z5NWzdhOyQ=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/samber/slog-common v0.18.1 h1:c0EipD/nVY9HG5shgm/XAs67mgpWDMF+MmtptdJNCkQ=
github.com/samber/slog-common v0.18.1/go.mod h1:QNZiNGKakvrfbJ2YglQXLCZauzkI9xZBjOhWFKS3IKk=
github.com/samber/slog-zerolog/v2 v2.7.3 h1:/MkPDl/tJhijN2GvB1MWwBn2FU8RiL3rQ8gpXkQm2EY=
@ -1530,8 +1533,6 @@ github.com/selectel/domains-go v1.1.0 h1:futG50J43ALLKQAnZk9H9yOtLGnSUh7c5hSvuC5
github.com/selectel/domains-go v1.1.0/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA=
github.com/selectel/go-selvpcclient/v3 v3.2.1 h1:ny6WIAMiHzKxOgOEnwcWE79wIQij1AHHylzPA41MXCw=
github.com/selectel/go-selvpcclient/v3 v3.2.1/go.mod h1:3EfSf8aEWyhspOGbvZ6mvnFg7JN5uckxNyBFPGWsXNQ=
github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE=
github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@ -1572,8 +1573,8 @@ github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZ
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
@ -1615,8 +1616,8 @@ github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNG
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1136/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1151 h1:SBbEaeCwhqmyAEEF5ubpg/2vv3RO6SdBsOSYhpnJaL4=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1151/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1164 h1:qEzZCZf1sgvvrZ8ngws0gZlyW+sOdY0K9VXGm4AcvTE=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1164/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 h1:kMIdSU5IvpOROh27ToVQ3hlm6ym3lCRs9tnGCOBoZqk=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136/go.mod h1:FpyIz3mymKaExVs6Fz27kxDBS42jqZn7vbACtxdeEH4=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
@ -1642,10 +1643,10 @@ github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8A
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
github.com/volcengine/volc-sdk-golang v1.0.206 h1:7NG8FCpvu9wbx+Z4I/p3tcTS2zdBqTZtJXgydunGy6g=
github.com/volcengine/volc-sdk-golang v1.0.206/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
github.com/vultr/govultr/v3 v3.19.1 h1:31rOP5Tz40AOc8h6Ws4ryzqAniUBffgRhy9uMG/EFvs=
github.com/vultr/govultr/v3 v3.19.1/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=
github.com/volcengine/volc-sdk-golang v1.0.207 h1:1OJ/nC92dF1URRoyO1AHSghCob12NT1PAA/GoK8uU18=
github.com/volcengine/volc-sdk-golang v1.0.207/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
github.com/vultr/govultr/v3 v3.20.0 h1:O+Om6gXpN6ehwAIIKq5DyGuekpyHaoRlwrxTb44bDzA=
github.com/vultr/govultr/v3 v3.20.0/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@ -1701,8 +1702,8 @@ go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
@ -1714,8 +1715,8 @@ go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@ -1725,8 +1726,8 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
@ -1771,8 +1772,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1916,8 +1917,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1947,8 +1948,8 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1969,8 +1970,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -2092,8 +2093,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -2111,8 +2112,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
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/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -2134,8 +2135,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -2217,8 +2218,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -2293,8 +2294,8 @@ google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60c
google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM=
google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ=
google.golang.org/api v0.233.0 h1:iGZfjXAJiUFSSaekVB7LzXl6tRfEKhUN7FkZN++07tI=
google.golang.org/api v0.233.0/go.mod h1:TCIVLLlcwunlMpZIhIp7Ltk77W+vUSdUKAAIlbxY44c=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -2435,10 +2436,12 @@ google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOl
google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f h1:tjZsroqekhC63+WMqzmWyW5Twj/ZfR5HAlpd5YQ1Vs0=
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197 h1:29cjnHVylHwTzH66WfFZqgSQgnxzvWE+jvBwpZCLRxY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -2481,8 +2484,8 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@ -2523,8 +2526,8 @@ gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/ns1/ns1-go.v2 v2.14.2 h1:wz/toj9U20wBrmYxW4vTz7sZWED+JJVRjUBBJ7CKrzI=
gopkg.in/ns1/ns1-go.v2 v2.14.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
gopkg.in/ns1/ns1-go.v2 v2.14.3 h1:Yn72GgB6AA9I4602AsLMtbC1ZKT5EUrKiG+IPS+Ovr0=
gopkg.in/ns1/ns1-go.v2 v2.14.3/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
@ -2560,8 +2563,8 @@ k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg=
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=

View file

@ -2,16 +2,14 @@ package acl
import (
"net"
"sync"
"time"
"github.com/oschwald/maxminddb-golang"
"github.com/puzpuzpuz/xsync/v3"
"github.com/rs/zerolog"
acl "github.com/yusing/go-proxy/internal/acl/types"
"github.com/puzpuzpuz/xsync/v4"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/logging/accesslog"
"github.com/yusing/go-proxy/internal/maxmind"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
)
@ -19,43 +17,24 @@ import (
type Config struct {
Default string `json:"default" validate:"omitempty,oneof=allow deny"` // default: allow
AllowLocal *bool `json:"allow_local"` // default: true
Allow []string `json:"allow"`
Deny []string `json:"deny"`
Allow Matchers `json:"allow"`
Deny Matchers `json:"deny"`
Log *accesslog.ACLLoggerConfig `json:"log"`
MaxMind *MaxMindConfig `json:"maxmind" validate:"omitempty"`
config
valErr gperr.Error
}
type (
MaxMindDatabaseType string
MaxMindConfig struct {
AccountID string `json:"account_id" validate:"required"`
LicenseKey string `json:"license_key" validate:"required"`
Database MaxMindDatabaseType `json:"database" validate:"required,oneof=geolite geoip2"`
logger zerolog.Logger
lastUpdate time.Time
db struct {
*maxminddb.Reader
sync.RWMutex
}
}
)
type config struct {
defaultAllow bool
allowLocal bool
allow []matcher
deny []matcher
ipCache *xsync.MapOf[string, *checkCache]
ipCache *xsync.Map[string, *checkCache]
logAllowed bool
logger *accesslog.AccessLogger
}
type checkCache struct {
*acl.IPInfo
*maxmind.IPInfo
allow bool
created time.Time
}
@ -63,7 +42,7 @@ type checkCache struct {
const cacheTTL = 1 * time.Minute
func (c *checkCache) Expired() bool {
return c.created.Add(cacheTTL).After(utils.TimeNow())
return c.created.Add(cacheTTL).Before(utils.TimeNow())
}
//TODO: add stats
@ -73,11 +52,6 @@ const (
ACLDeny = "deny"
)
const (
MaxMindGeoLite MaxMindDatabaseType = "geolite"
MaxMindGeoIP2 MaxMindDatabaseType = "geoip2"
)
func (c *Config) Validate() gperr.Error {
switch c.Default {
case "", ACLAllow:
@ -85,7 +59,8 @@ func (c *Config) Validate() gperr.Error {
case ACLDeny:
c.defaultAllow = false
default:
return gperr.New("invalid default value").Subject(c.Default)
c.valErr = gperr.New("invalid default value").Subject(c.Default)
return c.valErr
}
if c.AllowLocal != nil {
@ -94,55 +69,24 @@ func (c *Config) Validate() gperr.Error {
c.allowLocal = true
}
if c.MaxMind != nil {
c.MaxMind.logger = logging.With().Str("type", string(c.MaxMind.Database)).Logger()
}
if c.Log != nil {
c.logAllowed = c.Log.LogAllowed
}
errs := gperr.NewBuilder("syntax error")
c.allow = make([]matcher, 0, len(c.Allow))
c.deny = make([]matcher, 0, len(c.Deny))
for _, s := range c.Allow {
m, err := c.parseMatcher(s)
if err != nil {
errs.Add(err.Subject(s))
continue
}
c.allow = append(c.allow, m)
}
for _, s := range c.Deny {
m, err := c.parseMatcher(s)
if err != nil {
errs.Add(err.Subject(s))
continue
}
c.deny = append(c.deny, m)
if !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
}
if errs.HasError() {
c.allow = nil
c.deny = nil
return errMatcherFormat.With(errs.Error())
}
c.ipCache = xsync.NewMapOf[string, *checkCache]()
c.ipCache = xsync.NewMap[string, *checkCache]()
return nil
}
func (c *Config) Valid() bool {
return c != nil && (len(c.allow) > 0 || len(c.deny) > 0 || c.allowLocal)
return c != nil && c.valErr == nil
}
func (c *Config) Start(parent *task.Task) gperr.Error {
if c.MaxMind != nil {
if err := c.MaxMind.LoadMaxMindDB(parent); err != nil {
return err
}
}
if c.Log != nil {
logger, err := accesslog.NewAccessLogger(parent, c.Log)
if err != nil {
@ -150,10 +94,22 @@ func (c *Config) Start(parent *task.Task) gperr.Error {
}
c.logger = logger
}
if c.valErr != nil {
return c.valErr
}
logging.Info().
Str("default", c.Default).
Bool("allow_local", c.allowLocal).
Int("allow_rules", len(c.Allow)).
Int("deny_rules", len(c.Deny)).
Msg("ACL started")
return nil
}
func (c *config) cacheRecord(info *acl.IPInfo, allow bool) {
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
if common.ForceResolveCountry && info.City == nil {
maxmind.LookupCity(info)
}
c.ipCache.Store(info.Str, &checkCache{
IPInfo: info,
allow: allow,
@ -161,7 +117,7 @@ func (c *config) cacheRecord(info *acl.IPInfo, allow bool) {
})
}
func (c *config) log(info *acl.IPInfo, allowed bool) {
func (c *config) log(info *maxmind.IPInfo, allowed bool) {
if c.logger == nil {
return
}
@ -175,14 +131,13 @@ func (c *Config) IPAllowed(ip net.IP) bool {
return false
}
// always allow private and loopback
// loopback is not logged
// always allow loopback, not logged
if ip.IsLoopback() {
return true
}
if c.allowLocal && ip.IsPrivate() {
c.log(&acl.IPInfo{IP: ip, Str: ip.String()}, true)
c.log(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
return true
}
@ -193,21 +148,17 @@ func (c *Config) IPAllowed(ip net.IP) bool {
return record.allow
}
ipAndStr := &acl.IPInfo{IP: ip, Str: ipStr}
for _, m := range c.allow {
if m(ipAndStr) {
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
if c.Allow.Match(ipAndStr) {
c.log(ipAndStr, true)
c.cacheRecord(ipAndStr, true)
return true
}
}
for _, m := range c.deny {
if m(ipAndStr) {
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)

View file

@ -4,11 +4,17 @@ import (
"net"
"strings"
acl "github.com/yusing/go-proxy/internal/acl/types"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/maxmind"
)
type matcher func(*acl.IPInfo) bool
type MatcherFunc func(*maxmind.IPInfo) bool
type Matcher struct {
match MatcherFunc
}
type Matchers []Matcher
const (
MatcherTypeIP = "ip"
@ -17,6 +23,9 @@ const (
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",
@ -25,62 +34,66 @@ var errMatcherFormat = gperr.Multiline().AddLines(
"tz:Asia/Shanghai",
"country:GB",
)
var (
errSyntax = gperr.New("syntax error")
errInvalidIP = gperr.New("invalid IP")
errInvalidCIDR = gperr.New("invalid CIDR")
errMaxMindNotConfigured = gperr.New("MaxMind not configured")
)
func (cfg *Config) parseMatcher(s string) (matcher, gperr.Error) {
func (matcher *Matcher) Parse(s string) error {
parts := strings.Split(s, ":")
if len(parts) != 2 {
return nil, errSyntax
return errSyntax
}
switch parts[0] {
case MatcherTypeIP:
ip := net.ParseIP(parts[1])
if ip == nil {
return nil, errInvalidIP
return errInvalidIP
}
return matchIP(ip), nil
matcher.match = matchIP(ip)
case MatcherTypeCIDR:
_, net, err := net.ParseCIDR(parts[1])
if err != nil {
return nil, errInvalidCIDR
return errInvalidCIDR
}
return matchCIDR(net), nil
matcher.match = matchCIDR(net)
case MatcherTypeTimeZone:
if cfg.MaxMind == nil {
return nil, errMaxMindNotConfigured
}
return cfg.MaxMind.matchTimeZone(parts[1]), nil
matcher.match = matchTimeZone(parts[1])
case MatcherTypeCountry:
if cfg.MaxMind == nil {
return nil, errMaxMindNotConfigured
}
return cfg.MaxMind.matchISOCode(parts[1]), nil
matcher.match = matchISOCode(parts[1])
default:
return nil, errSyntax
return errSyntax
}
return nil
}
func matchIP(ip net.IP) matcher {
return func(ip2 *acl.IPInfo) bool {
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) matcher {
return func(ip *acl.IPInfo) bool {
func matchCIDR(n *net.IPNet) MatcherFunc {
return func(ip *maxmind.IPInfo) bool {
return n.Contains(ip.IP)
}
}
func (cfg *MaxMindConfig) matchTimeZone(tz string) matcher {
return func(ip *acl.IPInfo) bool {
city, ok := cfg.lookupCity(ip)
func matchTimeZone(tz string) MatcherFunc {
return func(ip *maxmind.IPInfo) bool {
city, ok := maxmind.LookupCity(ip)
if !ok {
return false
}
@ -88,9 +101,9 @@ func (cfg *MaxMindConfig) matchTimeZone(tz string) matcher {
}
}
func (cfg *MaxMindConfig) matchISOCode(iso string) matcher {
return func(ip *acl.IPInfo) bool {
city, ok := cfg.lookupCity(ip)
func matchISOCode(iso string) MatcherFunc {
return func(ip *maxmind.IPInfo) bool {
city, ok := maxmind.LookupCity(ip)
if !ok {
return false
}

View file

@ -0,0 +1,49 @@
package acl
import (
"net"
"reflect"
"testing"
maxmind "github.com/yusing/go-proxy/internal/maxmind/types"
"github.com/yusing/go-proxy/internal/utils"
)
func TestMatchers(t *testing.T) {
strMatchers := []string{
"ip:127.0.0.1",
"cidr:10.0.0.0/8",
}
var mathers Matchers
err := utils.Convert(reflect.ValueOf(strMatchers), reflect.ValueOf(&mathers), false)
if err != nil {
t.Fatal(err)
}
tests := []struct {
ip string
want bool
}{
{"127.0.0.1", true},
{"10.0.0.1", true},
{"127.0.0.2", false},
{"192.168.0.1", false},
{"11.0.0.1", false},
}
for _, test := range tests {
ip := net.ParseIP(test.ip)
if ip == nil {
t.Fatalf("invalid ip: %s", test.ip)
}
got := mathers.Match(&maxmind.IPInfo{
IP: ip,
Str: test.ip,
})
if got != test.want {
t.Errorf("mathers.Match(%s) = %v, want %v", test.ip, got, test.want)
}
}
}

View file

@ -1,281 +0,0 @@
package acl
import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/oschwald/maxminddb-golang"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/task"
)
var (
updateInterval = 24 * time.Hour
httpClient = &http.Client{
Timeout: 10 * time.Second,
}
ErrResponseNotOK = gperr.New("response not OK")
ErrDownloadFailure = gperr.New("download failure")
)
func dbPathImpl(dbType MaxMindDatabaseType) string {
if dbType == MaxMindGeoLite {
return filepath.Join(dataDir, "GeoLite2-City.mmdb")
}
return filepath.Join(dataDir, "GeoIP2-City.mmdb")
}
func dbURLimpl(dbType MaxMindDatabaseType) string {
if dbType == MaxMindGeoLite {
return "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"
}
return "https://download.maxmind.com/geoip/databases/GeoIP2-City/download?suffix=tar.gz"
}
func dbFilename(dbType MaxMindDatabaseType) string {
if dbType == MaxMindGeoLite {
return "GeoLite2-City.mmdb"
}
return "GeoIP2-City.mmdb"
}
func (cfg *MaxMindConfig) LoadMaxMindDB(parent task.Parent) gperr.Error {
if cfg.Database == "" {
return nil
}
path := dbPath(cfg.Database)
reader, err := maxmindDBOpen(path)
exists := true
if err != nil {
switch {
case errors.Is(err, os.ErrNotExist):
default:
// ignore invalid error, just download it again
var invalidErr maxminddb.InvalidDatabaseError
if !errors.As(err, &invalidErr) {
return gperr.Wrap(err)
}
}
exists = false
}
if !exists {
cfg.logger.Info().Msg("MaxMind DB not found/invalid, downloading...")
reader, err = cfg.download()
if err != nil {
return ErrDownloadFailure.With(err)
}
}
cfg.logger.Info().Msg("MaxMind DB loaded")
cfg.db.Reader = reader
go cfg.scheduleUpdate(parent)
return nil
}
func (cfg *MaxMindConfig) loadLastUpdate() {
f, err := os.Stat(dbPath(cfg.Database))
if err != nil {
return
}
cfg.lastUpdate = f.ModTime()
}
func (cfg *MaxMindConfig) setLastUpdate(t time.Time) {
cfg.lastUpdate = t
_ = os.Chtimes(dbPath(cfg.Database), t, t)
}
func (cfg *MaxMindConfig) scheduleUpdate(parent task.Parent) {
task := parent.Subtask("schedule_update", true)
ticker := time.NewTicker(updateInterval)
cfg.loadLastUpdate()
cfg.update()
defer func() {
ticker.Stop()
if cfg.db.Reader != nil {
cfg.db.Reader.Close()
}
task.Finish(nil)
}()
for {
select {
case <-task.Context().Done():
return
case <-ticker.C:
cfg.update()
}
}
}
func (cfg *MaxMindConfig) update() {
// check for update
cfg.logger.Info().Msg("checking for MaxMind DB update...")
remoteLastModified, err := cfg.checkLastest()
if err != nil {
cfg.logger.Err(err).Msg("failed to check MaxMind DB update")
return
}
if remoteLastModified.Equal(cfg.lastUpdate) {
cfg.logger.Info().Msg("MaxMind DB is up to date")
return
}
cfg.logger.Info().
Time("latest", remoteLastModified.Local()).
Time("current", cfg.lastUpdate).
Msg("MaxMind DB update available")
reader, err := cfg.download()
if err != nil {
cfg.logger.Err(err).Msg("failed to update MaxMind DB")
return
}
cfg.db.Lock()
cfg.db.Close()
cfg.db.Reader = reader
cfg.setLastUpdate(*remoteLastModified)
cfg.db.Unlock()
cfg.logger.Info().Msg("MaxMind DB updated")
}
func (cfg *MaxMindConfig) newReq(method string) (*http.Response, error) {
req, err := http.NewRequest(method, dbURL(cfg.Database), nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(cfg.AccountID, cfg.LicenseKey)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
func (cfg *MaxMindConfig) checkLastest() (lastModifiedT *time.Time, err error) {
resp, err := newReq(cfg, http.MethodHead)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode)
}
lastModified := resp.Header.Get("Last-Modified")
if lastModified == "" {
cfg.logger.Warn().Msg("MaxMind responded no last modified time, update skipped")
return nil, nil
}
lastModifiedTime, err := time.Parse(http.TimeFormat, lastModified)
if err != nil {
cfg.logger.Warn().Err(err).Msg("MaxMind responded invalid last modified time, update skipped")
return nil, err
}
return &lastModifiedTime, nil
}
func (cfg *MaxMindConfig) download() (*maxminddb.Reader, error) {
resp, err := newReq(cfg, http.MethodGet)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode)
}
path := dbPath(cfg.Database)
tmpPath := path + "-tmp.tar.gz"
file, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return nil, err
}
cfg.logger.Info().Msg("MaxMind DB downloading...")
_, err = io.Copy(file, resp.Body)
if err != nil {
file.Close()
return nil, err
}
file.Close()
// extract .tar.gz and move only the dbFilename to path
err = extractFileFromTarGz(tmpPath, dbFilename(cfg.Database), path)
if err != nil {
return nil, gperr.New("failed to extract database from archive").With(err)
}
// cleanup the tar.gz file
_ = os.Remove(tmpPath)
db, err := maxmindDBOpen(path)
if err != nil {
return nil, err
}
return db, nil
}
func extractFileFromTarGz(tarGzPath, targetFilename, destPath string) error {
f, err := os.Open(tarGzPath)
if err != nil {
return err
}
defer f.Close()
gzr, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
for {
hdr, err := tr.Next()
if err == io.EOF {
break // End of archive
}
if err != nil {
return err
}
// Only extract the file that matches targetFilename (basename match)
if filepath.Base(hdr.Name) == targetFilename {
outFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, hdr.FileInfo().Mode())
if err != nil {
return err
}
defer outFile.Close()
_, err = io.Copy(outFile, tr)
if err != nil {
return err
}
return nil // Done
}
}
return fmt.Errorf("file %s not found in archive", targetFilename)
}
var (
dataDir = common.DataDir
dbURL = dbURLimpl
dbPath = dbPathImpl
maxmindDBOpen = maxminddb.Open
newReq = (*MaxMindConfig).newReq
)

View file

@ -1,213 +0,0 @@
package acl
import (
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"github.com/oschwald/maxminddb-golang"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/task"
)
func Test_dbPath(t *testing.T) {
tmpDataDir := "/tmp/testdata"
oldDataDir := dataDir
dataDir = tmpDataDir
defer func() { dataDir = oldDataDir }()
tests := []struct {
name string
dbType MaxMindDatabaseType
want string
}{
{"GeoLite", MaxMindGeoLite, filepath.Join(tmpDataDir, "GeoLite2-City.mmdb")},
{"GeoIP2", MaxMindGeoIP2, filepath.Join(tmpDataDir, "GeoIP2-City.mmdb")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := dbPath(tt.dbType); got != tt.want {
t.Errorf("dbPath() = %v, want %v", got, tt.want)
}
})
}
}
func Test_dbURL(t *testing.T) {
tests := []struct {
name string
dbType MaxMindDatabaseType
want string
}{
{"GeoLite", MaxMindGeoLite, "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"},
{"GeoIP2", MaxMindGeoIP2, "https://download.maxmind.com/geoip/databases/GeoIP2-City/download?suffix=tar.gz"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := dbURL(tt.dbType); got != tt.want {
t.Errorf("dbURL() = %v, want %v", got, tt.want)
}
})
}
}
// --- Helper for MaxMindConfig ---
type testLogger struct{ zerolog.Logger }
func (testLogger) Info() *zerolog.Event { return &zerolog.Event{} }
func (testLogger) Warn() *zerolog.Event { return &zerolog.Event{} }
func (testLogger) Err(_ error) *zerolog.Event { return &zerolog.Event{} }
func Test_MaxMindConfig_newReq(t *testing.T) {
cfg := &MaxMindConfig{
AccountID: "testid",
LicenseKey: "testkey",
Database: MaxMindGeoLite,
logger: zerolog.Nop(),
}
// Patch httpClient to use httptest
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if u, p, ok := r.BasicAuth(); !ok || u != "testid" || p != "testkey" {
t.Errorf("basic auth not set correctly")
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
oldURL := dbURL
dbURL = func(MaxMindDatabaseType) string { return server.URL }
defer func() { dbURL = oldURL }()
resp, err := cfg.newReq(http.MethodGet)
if err != nil {
t.Fatalf("newReq() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("unexpected status: %v", resp.StatusCode)
}
}
func Test_MaxMindConfig_checkUpdate(t *testing.T) {
cfg := &MaxMindConfig{
AccountID: "id",
LicenseKey: "key",
Database: MaxMindGeoLite,
logger: zerolog.Nop(),
}
lastMod := time.Now().UTC().Format(http.TimeFormat)
buildTime := time.Now().Add(-time.Hour)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Last-Modified", lastMod)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
oldURL := dbURL
dbURL = func(MaxMindDatabaseType) string { return server.URL }
defer func() { dbURL = oldURL }()
latest, err := cfg.checkLastest()
if err != nil {
t.Fatalf("checkUpdate() error = %v", err)
}
if latest.Equal(buildTime) {
t.Errorf("expected update needed")
}
}
type fakeReadCloser struct {
firstRead bool
closed bool
}
func (c *fakeReadCloser) Read(p []byte) (int, error) {
if !c.firstRead {
c.firstRead = true
return strings.NewReader("FAKEMMDB").Read(p)
}
return 0, io.EOF
}
func (c *fakeReadCloser) Close() error {
c.closed = true
return nil
}
func Test_MaxMindConfig_download(t *testing.T) {
cfg := &MaxMindConfig{
AccountID: "id",
LicenseKey: "key",
Database: MaxMindGeoLite,
logger: zerolog.Nop(),
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, strings.NewReader("FAKEMMDB"))
}))
defer server.Close()
oldURL := dbURL
dbURL = func(MaxMindDatabaseType) string { return server.URL }
defer func() { dbURL = oldURL }()
tmpDir := t.TempDir()
oldDataDir := dataDir
dataDir = tmpDir
defer func() { dataDir = oldDataDir }()
// Patch maxminddb.Open to always succeed
origOpen := maxmindDBOpen
maxmindDBOpen = func(path string) (*maxminddb.Reader, error) {
return &maxminddb.Reader{}, nil
}
defer func() { maxmindDBOpen = origOpen }()
rw := &fakeReadCloser{}
oldNewReq := newReq
newReq = func(cfg *MaxMindConfig, method string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: rw,
}, nil
}
defer func() { newReq = oldNewReq }()
db, err := cfg.download()
if err != nil {
t.Fatalf("download() error = %v", err)
}
if db == nil {
t.Error("expected db instance")
}
if !rw.closed {
t.Error("expected rw to be closed")
}
}
func Test_MaxMindConfig_loadMaxMindDB(t *testing.T) {
// This test should cover both the path where DB exists and where it does not
// For brevity, only the non-existing path is tested here
cfg := &MaxMindConfig{
AccountID: "id",
LicenseKey: "key",
Database: MaxMindGeoLite,
logger: zerolog.Nop(),
}
oldOpen := maxmindDBOpen
maxmindDBOpen = func(path string) (*maxminddb.Reader, error) {
return &maxminddb.Reader{}, nil
}
defer func() { maxmindDBOpen = oldOpen }()
oldDBPath := dbPath
dbPath = func(MaxMindDatabaseType) string { return filepath.Join(t.TempDir(), "maxmind.mmdb") }
defer func() { dbPath = oldDBPath }()
task := task.RootTask("test")
defer task.Finish(nil)
err := cfg.LoadMaxMindDB(task)
if err != nil {
t.Errorf("loadMaxMindDB() error = %v", err)
}
}

View file

@ -1,7 +1,9 @@
package acl
import (
"io"
"net"
"time"
)
type TCPListener struct {
@ -9,6 +11,17 @@ type TCPListener struct {
lis net.Listener
}
type noConn struct{}
func (noConn) Read(b []byte) (int, error) { return 0, io.EOF }
func (noConn) Write(b []byte) (int, error) { return 0, io.EOF }
func (noConn) Close() error { return nil }
func (noConn) LocalAddr() net.Addr { return nil }
func (noConn) RemoteAddr() net.Addr { return nil }
func (noConn) SetDeadline(t time.Time) error { return nil }
func (noConn) SetReadDeadline(t time.Time) error { return nil }
func (noConn) SetWriteDeadline(t time.Time) error { return nil }
func (cfg *Config) WrapTCP(lis net.Listener) net.Listener {
if cfg == nil {
return lis
@ -32,11 +45,11 @@ func (s *TCPListener) Accept() (net.Conn, error) {
if !ok {
// Not a TCPAddr, drop
c.Close()
return nil, nil
return noConn{}, nil
}
if !s.acl.IPAllowed(addr.IP) {
c.Close()
return nil, nil
return noConn{}, nil
}
return c, nil
}

View file

@ -10,12 +10,12 @@ type UDPListener struct {
lis net.PacketConn
}
func (cfg *Config) WrapUDP(lis net.PacketConn) net.PacketConn {
if cfg == nil {
func (c *Config) WrapUDP(lis net.PacketConn) net.PacketConn {
if c == nil {
return lis
}
return &UDPListener{
acl: cfg,
acl: c,
lis: lis,
}
}

View file

@ -12,6 +12,7 @@ import (
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/metrics/uptime"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/utils/strutils"
"github.com/yusing/go-proxy/pkg"
@ -45,7 +46,7 @@ func (mux ServeMux) HandleFunc(methods, endpoint string, h any, requireAuth ...b
origHandler := handler
handler = func(w http.ResponseWriter, r *http.Request) {
if httpheaders.IsWebsocket(r.Header) {
httpheaders.SetWebsocketAllowedDomains(r.Header, matchDomains)
gpwebsocket.SetWebsocketAllowedDomains(r.Header, matchDomains)
}
origHandler(w, r)
}
@ -70,9 +71,15 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
mux.HandleFunc("GET", "/v1/stats", v1.Stats, true)
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/list", v1.ListRoutesHandler, true)
mux.HandleFunc("GET", "/v1/list/routes", v1.ListRoutesHandler, true)
mux.HandleFunc("GET", "/v1/list/route/{which}", v1.ListRouteHandler, true)
mux.HandleFunc("GET", "/v1/list/routes_by_provider", v1.ListRoutesByProviderHandler, true)
mux.HandleFunc("GET", "/v1/list/files", v1.ListFilesHandler, true)
mux.HandleFunc("GET", "/v1/list/homepage_config", v1.ListHomepageConfigHandler, true)
mux.HandleFunc("GET", "/v1/list/route_providers", v1.ListRouteProvidersHandler, true)
mux.HandleFunc("GET", "/v1/list/homepage_categories", v1.ListHomepageCategoriesHandler, true)
mux.HandleFunc("GET", "/v1/list/icons", v1.ListIconsHandler, true)
mux.HandleFunc("GET", "/v1/file/{type}/{filename}", v1.GetFileContent, true)
mux.HandleFunc("POST,PUT", "/v1/file/{type}/{filename}", v1.SetFileContent, true)
mux.HandleFunc("POST", "/v1/file/validate/{type}", v1.ValidateFile, true)

View file

@ -1,6 +1,7 @@
package v1
import (
"fmt"
"io"
"net/http"
"os"
@ -51,12 +52,12 @@ func (t FileType) GetPath(filename string) string {
func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
fileType = FileType(r.PathValue("type"))
if !fileType.IsValid() {
err = gphttp.ErrInvalidKey("type")
err = fmt.Errorf("invalid file type: %s", fileType)
return
}
filename = r.PathValue("filename")
if filename == "" {
err = gphttp.ErrMissingKey("filename")
err = fmt.Errorf("missing filename")
}
return
}

View file

@ -1,5 +0,0 @@
package dockerapi
import "time"
const reqTimeout = 10 * time.Second

View file

@ -18,7 +18,7 @@ type Container struct {
}
func Containers(w http.ResponseWriter, r *http.Request) {
serveHTTP[Container, []Container](w, r, GetContainers)
serveHTTP[Container](w, r, GetContainers)
}
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {

View file

@ -22,7 +22,7 @@ func Logs(w http.ResponseWriter, r *http.Request) {
until := query.Get("to")
levels := query.Get("levels") // TODO: implement levels
dockerClient, found, err := getDockerClient(w, server)
dockerClient, found, err := getDockerClient(server)
if err != nil {
gphttp.BadRequest(w, err.Error())
return

View file

@ -56,7 +56,7 @@ func getDockerClients() (DockerClients, gperr.Error) {
return dockerClients, connErrs.Error()
}
func getDockerClient(w http.ResponseWriter, server string) (*docker.SharedClient, bool, error) {
func getDockerClient(server string) (*docker.SharedClient, bool, error) {
cfg := config.GetInstance()
var host string
for name, h := range cfg.Value().Providers.Docker {
@ -98,7 +98,7 @@ func handleResult[V any, T ResultType[V]](w http.ResponseWriter, errs error, res
return
}
}
json.NewEncoder(w).Encode(result)
json.NewEncoder(w).Encode(result) //nolint
}
func serveHTTP[V any, T ResultType[V]](w http.ResponseWriter, r *http.Request, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
@ -119,6 +119,6 @@ func serveHTTP[V any, T ResultType[V]](w http.ResponseWriter, r *http.Request, g
})
} else {
result, err := getResult(r.Context(), dockerClients)
handleResult[V, T](w, err, result)
handleResult[V](w, err, result)
}
}

View file

@ -1,10 +1,8 @@
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"
@ -21,11 +19,11 @@ import (
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)
gphttp.MissingKey(w, "url or alias")
return
}
if url != "" && alias != "" {
gphttp.ClientError(w, gperr.New("url and alias are mutually exclusive"), http.StatusBadRequest)
gphttp.BadRequest(w, "url and alias are mutually exclusive")
return
}
@ -33,7 +31,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
if url != "" {
var iconURL homepage.IconURL
if err := iconURL.Parse(url); err != nil {
gphttp.ClientError(w, err, http.StatusBadRequest)
gphttp.ClientError(w, req, err, http.StatusBadRequest)
return
}
fetchResult := homepage.FetchFavIconFromURL(req.Context(), &iconURL)
@ -49,7 +47,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
// try with route.Icon
r, ok := routes.HTTP.Get(alias)
if !ok {
gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound)
gphttp.ValueNotFound(w, "route", alias)
return
}
@ -57,7 +55,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
hp := r.HomepageItem()
if hp.Icon != nil {
if hp.Icon.IconSource == homepage.IconSourceRelative {
result = homepage.FindIcon(req.Context(), r, hp.Icon.Value)
result = homepage.FindIcon(req.Context(), r, *hp.Icon.FullURL)
} else {
result = homepage.FetchFavIconFromURL(req.Context(), hp.Icon)
}

View file

@ -43,7 +43,7 @@ func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
gphttp.ClientError(w, err, http.StatusBadRequest)
gphttp.ClientError(w, r, err, http.StatusBadRequest)
return
}
r.Body.Close()
@ -53,21 +53,21 @@ func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
case HomepageOverrideItem:
var params HomepageOverrideItemParams
if err := json.Unmarshal(data, &params); err != nil {
gphttp.ClientError(w, err, http.StatusBadRequest)
gphttp.ClientError(w, r, err, http.StatusBadRequest)
return
}
overrides.OverrideItem(params.Which, &params.Value)
case HomepageOverrideItemsBatch:
var params HomepageOverrideItemsBatchParams
if err := json.Unmarshal(data, &params); err != nil {
gphttp.ClientError(w, err, http.StatusBadRequest)
gphttp.ClientError(w, r, err, http.StatusBadRequest)
return
}
overrides.OverrideItems(params.Value)
case HomepageOverrideItemVisible: // POST /v1/item_visible [a,b,c], false => hide a, b, c
var params HomepageOverrideItemVisibleParams
if err := json.Unmarshal(data, &params); err != nil {
gphttp.ClientError(w, err, http.StatusBadRequest)
gphttp.ClientError(w, r, err, http.StatusBadRequest)
return
}
if params.Value {
@ -78,7 +78,7 @@ func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
case HomepageOverrideCategoryOrder:
var params HomepageOverrideCategoryOrderParams
if err := json.Unmarshal(data, &params); err != nil {
gphttp.ClientError(w, err, http.StatusBadRequest)
gphttp.ClientError(w, r, err, http.StatusBadRequest)
return
}
overrides.SetCategoryOrder(params.Which, params.Value)

View file

@ -1,128 +0,0 @@
package v1
import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/yusing/go-proxy/internal"
"github.com/yusing/go-proxy/internal/common"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
"github.com/yusing/go-proxy/internal/route/routes"
route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
)
const (
ListRoute = "route"
ListRoutes = "routes"
ListFiles = "files"
ListMiddlewares = "middlewares"
ListMiddlewareTraces = "middleware_trace"
ListMatchDomains = "match_domains"
ListHomepageConfig = "homepage_config"
ListRouteProviders = "route_providers"
ListHomepageCategories = "homepage_categories"
ListIcons = "icons"
ListTasks = "tasks"
)
func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
what := r.PathValue("what")
if what == "" {
what = ListRoutes
}
which := r.PathValue("which")
switch what {
case ListRoute:
route := listRoute(which)
if route == nil {
http.NotFound(w, r)
} else {
gphttp.RespondJSON(w, r, route)
}
case ListRoutes:
gphttp.RespondJSON(w, r, routes.ByAlias(route.RouteType(r.FormValue("type"))))
case ListFiles:
listFiles(w, r)
case ListMiddlewares:
gphttp.RespondJSON(w, r, middleware.All())
case ListMiddlewareTraces:
gphttp.RespondJSON(w, r, middleware.GetAllTrace())
case ListMatchDomains:
gphttp.RespondJSON(w, r, cfg.Value().MatchDomains)
case ListHomepageConfig:
gphttp.RespondJSON(w, r, routes.HomepageConfig(r.FormValue("category"), r.FormValue("provider")))
case ListRouteProviders:
gphttp.RespondJSON(w, r, cfg.RouteProviderList())
case ListHomepageCategories:
gphttp.RespondJSON(w, r, routes.HomepageCategories())
case ListIcons:
limit, err := strconv.Atoi(r.FormValue("limit"))
if err != nil {
limit = 0
}
icons, err := internal.SearchIcons(r.FormValue("keyword"), limit)
if err != nil {
gphttp.ClientError(w, err)
return
}
if icons == nil {
icons = []string{}
}
gphttp.RespondJSON(w, r, icons)
case ListTasks:
gphttp.RespondJSON(w, r, task.DebugTaskList())
default:
gphttp.BadRequest(w, fmt.Sprintf("invalid what: %s", what))
}
}
// if which is "all" or empty, return map[string]Route of all routes
// otherwise, return a single Route with alias which or nil if not found.
func listRoute(which string) any {
if which == "" || which == "all" {
return routes.ByAlias()
}
routes := routes.ByAlias()
route, ok := routes[which]
if !ok {
return nil
}
return route
}
func listFiles(w http.ResponseWriter, r *http.Request) {
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
if err != nil {
gphttp.ServerError(w, r, err)
return
}
resp := map[FileType][]string{
FileTypeConfig: make([]string, 0),
FileTypeProvider: make([]string, 0),
FileTypeMiddleware: make([]string, 0),
}
for _, file := range files {
t := fileType(file)
file = strings.TrimPrefix(file, common.ConfigBasePath+"/")
resp[t] = append(resp[t], file)
}
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
if err != nil {
gphttp.ServerError(w, r, err)
return
}
for _, mid := range mids {
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
}
gphttp.RespondJSON(w, r, resp)
}

View file

@ -0,0 +1,41 @@
package v1
import (
"net/http"
"strings"
"github.com/yusing/go-proxy/internal/common"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/utils"
)
func ListFilesHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
if err != nil {
gphttp.ServerError(w, r, err)
return
}
resp := map[FileType][]string{
FileTypeConfig: make([]string, 0),
FileTypeProvider: make([]string, 0),
FileTypeMiddleware: make([]string, 0),
}
for _, file := range files {
t := fileType(file)
file = strings.TrimPrefix(file, common.ConfigBasePath+"/")
resp[t] = append(resp[t], file)
}
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
if err != nil {
gphttp.ServerError(w, r, err)
return
}
for _, mid := range mids {
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
}
gphttp.RespondJSON(w, r, resp)
}

View file

@ -0,0 +1,13 @@
package v1
import (
"net/http"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/route/routes"
)
func ListHomepageCategoriesHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
gphttp.RespondJSON(w, r, routes.HomepageCategories())
}

View file

@ -0,0 +1,13 @@
package v1
import (
"net/http"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/route/routes"
)
func ListHomepageConfigHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
gphttp.RespondJSON(w, r, routes.HomepageConfig(r.FormValue("category"), r.FormValue("provider")))
}

View file

@ -0,0 +1,23 @@
package v1
import (
"net/http"
"strconv"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/net/gphttp"
)
func ListIconsHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
limit, err := strconv.Atoi(r.FormValue("limit"))
if err != nil {
limit = 0
}
icons, err := homepage.SearchIcons(r.FormValue("keyword"), limit)
if err != nil {
gphttp.ClientError(w, r, err)
return
}
gphttp.RespondJSON(w, r, icons)
}

View file

@ -0,0 +1,19 @@
package v1
import (
"net/http"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/route/routes"
)
func ListRouteHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
which := r.PathValue("which")
route, ok := routes.Get(which)
if ok {
gphttp.RespondJSON(w, r, route)
} else {
gphttp.RespondJSON(w, r, nil)
}
}

View file

@ -0,0 +1,23 @@
package v1
import (
"net/http"
"time"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
)
func ListRouteProvidersHandler(cfgInstance config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
if httpheaders.IsWebsocket(r.Header) {
gpwebsocket.Periodic(w, r, 5*time.Second, func(conn *websocket.Conn) error {
return wsjson.Write(r.Context(), conn, cfgInstance.RouteProviderList())
})
} else {
gphttp.RespondJSON(w, r, cfgInstance.RouteProviderList())
}
}

View file

@ -0,0 +1,25 @@
package v1
import (
"net/http"
"slices"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/route/routes"
)
func ListRoutesHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
rts := make([]routes.Route, 0)
provider := r.FormValue("provider")
if provider == "" {
gphttp.RespondJSON(w, r, slices.Collect(routes.Iter))
return
}
for r := range routes.Iter {
if r.ProviderName() == provider {
rts = append(rts, r)
}
}
gphttp.RespondJSON(w, r, rts)
}

View file

@ -0,0 +1,13 @@
package v1
import (
"net/http"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/route/routes"
)
func ListRoutesByProviderHandler(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
gphttp.RespondJSON(w, r, routes.ByProvider())
}

View file

@ -20,27 +20,27 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
name := q.Get("name")
if name == "" {
gphttp.ClientError(w, gphttp.ErrMissingKey("name"))
gphttp.MissingKey(w, "name")
return
}
host := q.Get("host")
if host == "" {
gphttp.ClientError(w, gphttp.ErrMissingKey("host"))
gphttp.MissingKey(w, "host")
return
}
portStr := q.Get("port")
if portStr == "" {
gphttp.ClientError(w, gphttp.ErrMissingKey("port"))
gphttp.MissingKey(w, "port")
return
}
port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 {
gphttp.ClientError(w, gphttp.ErrInvalidKey("port"))
gphttp.InvalidKey(w, "port")
return
}
hostport := fmt.Sprintf("%s:%d", host, port)
if _, ok := config.GetInstance().GetAgent(hostport); ok {
gphttp.ClientError(w, gphttp.ErrAlreadyExists("agent", hostport), http.StatusConflict)
gphttp.KeyAlreadyExists(w, "agent", hostport)
return
}
t := q.Get("type")
@ -48,10 +48,10 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
case "docker", "system":
break
case "":
gphttp.ClientError(w, gphttp.ErrMissingKey("type"))
gphttp.MissingKey(w, "type")
return
default:
gphttp.ClientError(w, gphttp.ErrInvalidKey("type"))
gphttp.InvalidKey(w, "type")
return
}
@ -109,13 +109,13 @@ func VerifyNewAgent(w http.ResponseWriter, r *http.Request) {
}
if err := json.Unmarshal(clientPEMData, &data); err != nil {
gphttp.ClientError(w, err, http.StatusBadRequest)
gphttp.ClientError(w, r, err)
return
}
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(data.Host, data.CA, data.Client)
if err != nil {
gphttp.ClientError(w, err)
gphttp.ClientError(w, r, err)
return
}
@ -127,7 +127,7 @@ func VerifyNewAgent(w http.ResponseWriter, r *http.Request) {
filename, ok := certs.AgentCertsFilepath(data.Host)
if !ok {
gphttp.ClientError(w, gphttp.ErrInvalidKey("host"))
gphttp.InvalidKey(w, "host")
return
}

View file

@ -1,64 +0,0 @@
package query
import (
"encoding/json"
"fmt"
"io"
"net/http"
v1 "github.com/yusing/go-proxy/internal/api/v1"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
)
func ReloadServer() gperr.Error {
resp, err := gphttp.Post(common.APIHTTPURL+"/v1/reload", "", nil)
if err != nil {
return gperr.Wrap(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
failure := gperr.Errorf("server reload status %v", resp.StatusCode)
body, err := io.ReadAll(resp.Body)
if err != nil {
return failure.With(err)
}
reloadErr := string(body)
return failure.Withf(reloadErr)
}
return nil
}
func List[T any](what string) (_ T, outErr gperr.Error) {
resp, err := gphttp.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, what))
if err != nil {
outErr = gperr.Wrap(err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
outErr = gperr.Errorf("list %s: failed, status %v", what, resp.StatusCode)
return
}
var res T
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
outErr = gperr.Wrap(err)
return
}
return res, nil
}
func ListRoutes() (map[string]map[string]any, gperr.Error) {
return List[map[string]map[string]any](v1.ListRoutes)
}
func ListMiddlewareTraces() (middleware.Traces, gperr.Error) {
return List[middleware.Traces](v1.ListMiddlewareTraces)
}
func DebugListTasks() (map[string]any, gperr.Error) {
return List[map[string]any](v1.ListTasks)
}

View file

@ -38,22 +38,35 @@ func IsOIDCEnabled() bool {
return common.OIDCIssuerURL != ""
}
type nextHandler struct{}
var nextHandlerContextKey = nextHandler{}
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
if IsEnabled() {
if !IsEnabled() {
return next
}
return func(w http.ResponseWriter, r *http.Request) {
if err := defaultAuth.CheckToken(r); err != nil {
gphttp.ClientError(w, err, http.StatusUnauthorized)
} else {
gphttp.Unauthorized(w, err.Error())
return
}
next(w, r)
}
}
}
return next
}
func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
if err := defaultAuth.CheckToken(r); err != nil {
http.Redirect(w, r, "/v1/auth/login", http.StatusFound)
func ProceedNext(w http.ResponseWriter, r *http.Request) {
next, ok := r.Context().Value(nextHandlerContextKey).(http.HandlerFunc)
if ok {
next(w, r)
} else {
w.WriteHeader(http.StatusOK)
}
}
func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
if err := defaultAuth.CheckToken(r); err != nil {
defaultAuth.LoginHandler(w, r)
} else {
w.WriteHeader(http.StatusOK)
}

View file

@ -1,11 +1,13 @@
package auth
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
@ -19,6 +21,10 @@ type oauthRefreshToken struct {
Username string `json:"username"`
RefreshToken string `json:"refresh_token"`
Expiry time.Time `json:"expiry"`
result *RefreshResult
err error
mu sync.Mutex
}
type Session struct {
@ -27,6 +33,12 @@ type Session struct {
Groups []string `json:"groups"`
}
type RefreshResult struct {
newSession Session
jwt string
jwtExpiry time.Time
}
type sessionClaims struct {
Session
jwt.RegisteredClaims
@ -34,11 +46,11 @@ type sessionClaims struct {
type sessionID string
var oauthRefreshTokens jsonstore.MapStore[oauthRefreshToken]
var oauthRefreshTokens jsonstore.MapStore[*oauthRefreshToken]
var (
defaultRefreshTokenExpiry = 30 * 24 * time.Hour // 1 month
refreshBefore = 30 * time.Second
sessionInvalidateDelay = 3 * time.Second
)
var (
@ -50,7 +62,7 @@ const sessionTokenIssuer = "GoDoxy"
func init() {
if IsOIDCEnabled() {
oauthRefreshTokens = jsonstore.Store[oauthRefreshToken]("oauth_refresh_tokens")
oauthRefreshTokens = jsonstore.Store[*oauthRefreshToken]("oauth_refresh_tokens")
}
}
@ -61,7 +73,7 @@ func (token *oauthRefreshToken) expired() bool {
func newSessionID() sessionID {
b := make([]byte, 32)
_, _ = rand.Read(b)
return sessionID(base64.StdEncoding.EncodeToString(b))
return sessionID(hex.EncodeToString(b))
}
func newSession(username string, groups []string) Session {
@ -72,26 +84,26 @@ func newSession(username string, groups []string) Session {
}
}
// getOnceOAuthRefreshToken returns the refresh token for the given session.
//
// The token is removed from the store after retrieval.
func getOnceOAuthRefreshToken(claims *Session) (*oauthRefreshToken, bool) {
// getOAuthRefreshToken returns the refresh token for the given session.
func getOAuthRefreshToken(claims *Session) (*oauthRefreshToken, bool) {
token, ok := oauthRefreshTokens.Load(string(claims.SessionID))
if !ok {
return nil, false
}
invalidateOAuthRefreshToken(claims.SessionID)
if token.expired() {
invalidateOAuthRefreshToken(claims.SessionID)
return nil, false
}
if claims.Username != token.Username {
return nil, false
}
return &token, true
return token, true
}
func storeOAuthRefreshToken(sessionID sessionID, username, token string) {
oauthRefreshTokens.Store(string(sessionID), oauthRefreshToken{
oauthRefreshTokens.Store(string(sessionID), &oauthRefreshToken{
Username: username,
RefreshToken: token,
Expiry: time.Now().Add(defaultRefreshTokenExpiry),
@ -118,7 +130,7 @@ func (auth *OIDCProvider) setSessionTokenCookie(w http.ResponseWriter, r *http.R
logging.Err(err).Msg("failed to sign session token")
return
}
setTokenCookie(w, r, CookieOauthSessionToken, signed, common.APIJWTTokenTTL)
SetTokenCookie(w, r, CookieOauthSessionToken, signed, common.APIJWTTokenTTL)
}
func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionClaims, valid bool, err error) {
@ -135,51 +147,75 @@ func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionCla
return claims, sessionToken.Valid && claims.Issuer == sessionTokenIssuer, nil
}
func (auth *OIDCProvider) TryRefreshToken(w http.ResponseWriter, r *http.Request, sessionJWT string) error {
func (auth *OIDCProvider) TryRefreshToken(ctx context.Context, sessionJWT string) (*RefreshResult, error) {
// verify the session cookie
claims, valid, err := auth.parseSessionJWT(sessionJWT)
if err != nil {
return fmt.Errorf("%w: %w", ErrInvalidSessionToken, err)
return nil, fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrInvalidSessionToken, err)
}
if !valid {
return ErrInvalidSessionToken
return nil, ErrInvalidSessionToken
}
// check if refresh is possible
refreshToken, ok := getOnceOAuthRefreshToken(&claims.Session)
refreshToken, ok := getOAuthRefreshToken(&claims.Session)
if !ok {
return errNoRefreshToken
return nil, errNoRefreshToken
}
if !auth.checkAllowed(claims.Username, claims.Groups) {
return ErrUserNotAllowed
return nil, ErrUserNotAllowed
}
return auth.doRefreshToken(ctx, refreshToken, &claims.Session)
}
func (auth *OIDCProvider) doRefreshToken(ctx context.Context, refreshToken *oauthRefreshToken, claims *Session) (*RefreshResult, error) {
refreshToken.mu.Lock()
defer refreshToken.mu.Unlock()
// already refreshed
// this must be called after refresh but before invalidate
if refreshToken.result != nil || refreshToken.err != nil {
return refreshToken.result, refreshToken.err
}
// this step refreshes the token
// see https://cs.opensource.google/go/x/oauth2/+/refs/tags/v0.29.0:oauth2.go;l=313
newToken, err := auth.oauthConfig.TokenSource(r.Context(), &oauth2.Token{
newToken, err := auth.oauthConfig.TokenSource(ctx, &oauth2.Token{
RefreshToken: refreshToken.RefreshToken,
}).Token()
if err != nil {
return fmt.Errorf("%w: %w", ErrRefreshTokenFailure, err)
refreshToken.err = fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrRefreshTokenFailure, err)
return nil, refreshToken.err
}
idTokenJWT, idToken, err := auth.getIdToken(r.Context(), newToken)
idTokenJWT, idToken, err := auth.getIdToken(ctx, newToken)
if err != nil {
return err
refreshToken.err = fmt.Errorf("session: %s - %w: %w", claims.SessionID, ErrRefreshTokenFailure, err)
return nil, refreshToken.err
}
// in case there're multiple requests for the same session to refresh
// invalidate the token after a short delay
go func() {
<-time.After(sessionInvalidateDelay)
invalidateOAuthRefreshToken(claims.SessionID)
}()
sessionID := newSessionID()
logging.Debug().Str("username", claims.Username).Time("expiry", newToken.Expiry).Msg("refreshed token")
storeOAuthRefreshToken(sessionID, claims.Username, newToken.RefreshToken)
// set new idToken and new sessionToken
auth.setIDTokenCookie(w, r, idTokenJWT, time.Until(idToken.Expiry))
auth.setSessionTokenCookie(w, r, Session{
refreshToken.result = &RefreshResult{
newSession: Session{
SessionID: sessionID,
Username: claims.Username,
Groups: claims.Groups,
})
return nil
},
jwt: idTokenJWT,
jwtExpiry: idToken.Expiry,
}
return refreshToken.result, nil
}

View file

@ -13,6 +13,7 @@ import (
"github.com/coreos/go-oidc/v3/oidc"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/utils"
@ -47,7 +48,12 @@ const (
OIDCLogoutPath = "/auth/logout"
)
var errMissingIDToken = errors.New("missing id_token field from oauth token")
var (
errMissingIDToken = errors.New("missing id_token field from oauth token")
ErrMissingOAuthToken = gperr.New("missing oauth token")
ErrInvalidOAuthToken = gperr.New("invalid oauth token")
)
// generateState generates a random string for OIDC state.
const oidcStateLength = 32
@ -62,7 +68,10 @@ func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, all
if len(allowedUsers)+len(allowedGroups) == 0 {
return nil, errors.New("oidc.allowed_users or oidc.allowed_groups are both empty")
}
provider, err := oidc.NewProvider(context.Background(), issuerURL)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
provider, err := oidc.NewProvider(ctx, issuerURL)
if err != nil {
return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err)
}
@ -133,6 +142,14 @@ func (auth *OIDCProvider) getIdToken(ctx context.Context, oauthToken *oauth2.Tok
}
func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "" {
r.URL.Path = OIDCAuthInitPath
}
if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" {
r.URL.Scheme = "https"
http.Redirect(w, r, r.URL.String(), http.StatusFound)
return
}
switch r.URL.Path {
case OIDCAuthInitPath:
auth.LoginHandler(w, r)
@ -148,18 +165,25 @@ func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
// check for session token
sessionToken, err := r.Cookie(CookieOauthSessionToken)
if err == nil { // session token exists
result, err := auth.TryRefreshToken(r.Context(), sessionToken.Value)
// redirect back to where they requested
// when token refresh is ok
if err == nil {
err = auth.TryRefreshToken(w, r, sessionToken.Value)
if err != nil {
logging.Debug().Err(err).Msg("failed to refresh token")
auth.clearCookie(w, r)
auth.setIDTokenCookie(w, r, result.jwt, time.Until(result.jwtExpiry))
auth.setSessionTokenCookie(w, r, result.newSession)
ProceedNext(w, r)
return
}
// clear cookies then redirect to home
logging.Err(err).Msg("failed to refresh token")
auth.clearCookie(w, r)
http.Redirect(w, r, "/", http.StatusFound)
return
}
state := generateState()
setTokenCookie(w, r, CookieOauthState, state, 300*time.Second)
SetTokenCookie(w, r, CookieOauthState, state, 300*time.Second)
// redirect user to Idp
http.Redirect(w, r, auth.oauthConfig.AuthCodeURL(state, optRedirectPostAuth(r)), http.StatusFound)
}
@ -170,7 +194,7 @@ func parseClaims(idToken *oidc.IDToken) (*IDTokenClaims, error) {
return nil, fmt.Errorf("failed to parse claims: %w", err)
}
if claim.Username == "" {
return nil, fmt.Errorf("missing username in ID token")
return nil, errors.New("missing username in ID token")
}
return &claim, nil
}
@ -284,12 +308,12 @@ func (auth *OIDCProvider) LogoutHandler(w http.ResponseWriter, r *http.Request)
}
func (auth *OIDCProvider) setIDTokenCookie(w http.ResponseWriter, r *http.Request, jwt string, ttl time.Duration) {
setTokenCookie(w, r, CookieOauthToken, jwt, ttl)
SetTokenCookie(w, r, CookieOauthToken, jwt, ttl)
}
func (auth *OIDCProvider) clearCookie(w http.ResponseWriter, r *http.Request) {
clearTokenCookie(w, r, CookieOauthToken)
clearTokenCookie(w, r, CookieOauthSessionToken)
ClearTokenCookie(w, r, CookieOauthToken)
ClearTokenCookie(w, r, CookieOauthSessionToken)
}
// handleTestCallback handles OIDC callback in test environment.
@ -306,7 +330,7 @@ func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Requ
}
// Create test JWT token
setTokenCookie(w, r, CookieOauthToken, "test", time.Hour)
SetTokenCookie(w, r, CookieOauthToken, "test", time.Hour)
http.Redirect(w, r, "/", http.StatusFound)
}

View file

@ -1,7 +1,6 @@
package auth
import (
"context"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
@ -24,7 +23,7 @@ import (
func setupMockOIDC(t *testing.T) {
t.Helper()
provider := (&oidc.ProviderConfig{}).NewProvider(context.TODO())
provider := (&oidc.ProviderConfig{}).NewProvider(t.Context())
defaultAuth = &OIDCProvider{
oauthConfig: &oauth2.Config{
ClientID: "test-client",
@ -104,7 +103,7 @@ func setupProvider(t *testing.T) *provider {
t.Cleanup(ts.Close)
// Create a test OIDCProvider.
providerCtx := oidc.ClientContext(context.Background(), ts.Client())
providerCtx := oidc.ClientContext(t.Context(), ts.Client())
keySet := oidc.NewRemoteKeySet(providerCtx, ts.URL+"/.well-known/jwks.json")
return &provider{

View file

@ -119,7 +119,7 @@ func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http
gphttp.ServerError(w, r, err)
return
}
setTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL)
SetTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL)
w.WriteHeader(http.StatusOK)
}
@ -128,7 +128,7 @@ func (auth *UserPassAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {
}
func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
clearTokenCookie(w, r, auth.TokenCookieName())
ClearTokenCookie(w, r, auth.TokenCookieName())
http.Redirect(w, r, "/", http.StatusFound)
}

View file

@ -98,7 +98,7 @@ func TestUserPassLoginCallbackHandler(t *testing.T) {
Host: "app.example.com",
Body: io.NopCloser(bytes.NewReader(Must(json.Marshal(tt.creds)))),
}
auth.LoginHandler(w, req)
auth.PostAuthCallbackHandler(w, req)
if tt.wantErr {
ExpectEqual(t, w.Code, http.StatusUnauthorized)
} else {

View file

@ -10,22 +10,21 @@ import (
)
var (
ErrMissingOAuthToken = gperr.New("missing oauth token")
ErrMissingSessionToken = gperr.New("missing session token")
ErrInvalidOAuthToken = gperr.New("invalid oauth token")
ErrInvalidSessionToken = gperr.New("invalid session token")
ErrUserNotAllowed = gperr.New("user not allowed")
)
func IsFrontend(r *http.Request) bool {
return r.Host == common.APIHTTPAddr
}
func requestHost(r *http.Request) string {
// check if it's from backend
switch r.Host {
case common.APIHTTPAddr:
// use XFH
if IsFrontend(r) {
return r.Header.Get("X-Forwarded-Host")
default:
return r.Host
}
return r.Host
}
// cookieDomain returns the fully qualified domain name of the request host
@ -45,7 +44,7 @@ func cookieDomain(r *http.Request) string {
return strutils.JoinRune(parts, '.')
}
func setTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {
func SetTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: value,
@ -58,7 +57,7 @@ func setTokenCookie(w http.ResponseWriter, r *http.Request, name, value string,
})
}
func clearTokenCookie(w http.ResponseWriter, r *http.Request, name string) {
func ClearTokenCookie(w http.ResponseWriter, r *http.Request, name string) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: "",

View file

@ -13,7 +13,6 @@ import (
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
type Config struct {
@ -70,7 +69,7 @@ func (cfg *Config) Validate() gperr.Error {
if !ok {
b.Add(ErrUnknownProvider.
Subject(cfg.Provider).
Withf(strutils.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
With(gperr.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
} else {
_, err := providerConstructor(cfg.Options)
if err != nil {

View file

@ -9,14 +9,15 @@ import (
"path"
"reflect"
"sort"
"sync"
"time"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/notif"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
@ -31,8 +32,6 @@ type (
legoCert *certificate.Resource
tlsCert *tls.Certificate
certExpiries CertExpiries
obtainMu sync.Mutex
}
CertExpiries map[string]time.Time
@ -193,8 +192,18 @@ func (p *Provider) ScheduleRenewal(parent task.Parent) {
if err := p.renewIfNeeded(); err != nil {
gperr.LogWarn("cert renew failed", err)
lastErrOn = time.Now()
notif.Notify(&notif.LogMessage{
Level: zerolog.ErrorLevel,
Title: "SSL certificate renewal failed",
Body: notif.MessageBody(err.Error()),
})
continue
}
notif.Notify(&notif.LogMessage{
Level: zerolog.InfoLevel,
Title: "SSL certificate renewed",
Body: notif.ListBody(p.cfg.Domains),
})
// Reset on success
lastErrOn = time.Time{}
renewalTime = p.ShouldRenewOn()

View file

@ -45,6 +45,6 @@ oauth2_config:
testYaml = testYaml[1:] // remove first \n
opt := make(map[string]any)
require.NoError(t, yaml.Unmarshal([]byte(testYaml), &opt))
require.NoError(t, utils.Deserialize(opt, cfg))
require.Equal(t, cfg, cfgExpected)
require.NoError(t, utils.MapUnmarshalValidate(opt, cfg))
require.Equal(t, cfgExpected, cfg)
}

View file

@ -16,7 +16,7 @@ func DNSProvider[CT any, PT challenge.Provider](
) Generator {
return func(opt map[string]any) (challenge.Provider, gperr.Error) {
cfg := defaultCfg()
err := utils.Deserialize(opt, &cfg)
err := utils.MapUnmarshalValidate(opt, &cfg)
if err != nil {
return nil, err
}

View file

@ -15,8 +15,8 @@ const (
ConfigExampleFileName = "config.example.yml"
ConfigPath = ConfigBasePath + "/" + ConfigFileName
IconListCachePath = ConfigBasePath + "/.icon_list_cache.json"
IconCachePath = ConfigBasePath + "/.icon_cache.json"
DataDir = "data"
IconListCachePath = DataDir + "/.icon_list_cache.json"
NamespaceHomepageOverrides = ".homepage"
NamespaceIconCache = ".icon_cache"
@ -25,14 +25,12 @@ const (
ComposeFileName = "compose.yml"
ComposeExampleFileName = "compose.example.yml"
DataDir = "data"
ErrorPagesBasePath = "error_pages"
)
var RequiredDirectories = []string{
ConfigBasePath,
DataDir,
ErrorPagesBasePath,
MiddlewareComposeBasePath,
}

View file

@ -3,8 +3,7 @@ package common
import (
"crypto/rand"
"encoding/base64"
"github.com/rs/zerolog/log"
"log"
)
func decodeJWTKey(key string) []byte {
@ -13,7 +12,7 @@ func decodeJWTKey(key string) []byte {
}
bytes, err := base64.StdEncoding.DecodeString(key)
if err != nil {
log.Fatal().Str("key", key).Err(err).Msg("failed to decode secret")
log.Fatalf("failed to decode secret: %s", err)
}
return bytes
}
@ -22,7 +21,7 @@ func RandomJWTKey() []byte {
key := make([]byte, 32)
_, err := rand.Read(key)
if err != nil {
log.Fatal().Err(err).Msg("failed to generate random jwt key")
log.Fatalf("failed to generate random jwt key: %s", err)
}
return key
}

View file

@ -2,13 +2,13 @@ package common
import (
"fmt"
"log"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
@ -58,6 +58,8 @@ var (
MetricsDisableDisk = GetEnvBool("METRICS_DISABLE_DISK", false)
MetricsDisableNetwork = GetEnvBool("METRICS_DISABLE_NETWORK", false)
MetricsDisableSensors = GetEnvBool("METRICS_DISABLE_SENSORS", false)
ForceResolveCountry = GetEnvBool("FORCE_RESOLVE_COUNTRY", false)
)
func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T {
@ -76,14 +78,16 @@ func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T
if err == nil {
return parsed
}
log.Fatal().Err(err).Msgf("env %s: invalid %T value: %s", key, parsed, value)
log.Fatalf("env %s: invalid %T value: %s", key, parsed, value)
return defaultValue
}
func GetEnvString(key string, defaultValue string) string {
return GetEnv(key, defaultValue, func(s string) (string, error) {
func stringstring(s string) (string, error) {
return s, nil
})
}
func GetEnvString(key string, defaultValue string) string {
return GetEnv(key, defaultValue, stringstring)
}
func GetEnvBool(key string, defaultValue bool) bool {
@ -101,7 +105,7 @@ func GetAddrEnv(key, defaultValue, scheme string) (addr, host string, portInt in
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
log.Fatal().Msgf("env %s: invalid address: %s", key, addr)
log.Fatalf("env %s: invalid address: %s", key, addr)
}
if host == "" {
host = "localhost"
@ -109,7 +113,7 @@ func GetAddrEnv(key, defaultValue, scheme string) (addr, host string, portInt in
fullURL = fmt.Sprintf("%s://%s:%s", scheme, host, port)
portInt, err = strconv.Atoi(port)
if err != nil {
log.Fatal().Msgf("env %s: invalid port: %s", key, port)
log.Fatalf("env %s: invalid port: %s", key, port)
}
return
}

View file

@ -40,7 +40,7 @@ func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PE
var agentCfg agent.AgentConfig
agentCfg.Addr = host
err := agentCfg.StartWithCerts(cfg.Task(), ca.Cert, client.Cert, client.Key)
err := agentCfg.StartWithCerts(cfg.Task().Context(), ca.Cert, client.Cert, client.Key)
if err != nil {
return 0, gperr.Wrap(err, "failed to start agent")
}

View file

@ -9,6 +9,7 @@ import (
"sync"
"time"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/api"
autocert "github.com/yusing/go-proxy/internal/autocert"
"github.com/yusing/go-proxy/internal/common"
@ -16,12 +17,15 @@ import (
"github.com/yusing/go-proxy/internal/entrypoint"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/maxmind"
"github.com/yusing/go-proxy/internal/net/gphttp/server"
"github.com/yusing/go-proxy/internal/notif"
"github.com/yusing/go-proxy/internal/proxmox"
proxy "github.com/yusing/go-proxy/internal/route/provider"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
F "github.com/yusing/go-proxy/internal/utils/functional"
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
"github.com/yusing/go-proxy/internal/watcher"
"github.com/yusing/go-proxy/internal/watcher/events"
)
@ -114,7 +118,7 @@ func Reload() gperr.Error {
err := newCfg.load()
if err != nil {
newCfg.task.Finish(err)
return gperr.New("using last config").With(err)
return gperr.New(ansi.Warning("using last config")).With(err)
}
// cancel all current subtasks -> wait
@ -219,7 +223,7 @@ func (cfg *Config) load() gperr.Error {
}
model := config.DefaultConfig()
if err := utils.DeserializeYAML(data, model); err != nil {
if err := utils.UnmarshalValidateYAML(data, model); err != nil {
gperr.LogFatal(errMsg, err)
}
@ -227,8 +231,10 @@ func (cfg *Config) load() gperr.Error {
errs := gperr.NewBuilder(errMsg)
errs.Add(cfg.entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
errs.Add(cfg.initMaxMind(model.Providers.MaxMind))
cfg.initNotification(model.Providers.Notification)
errs.Add(cfg.initAutoCert(model.AutoCert))
errs.Add(cfg.initProxmox(model.Providers.Proxmox))
errs.Add(cfg.loadRouteProviders(&model.Providers))
cfg.value = model
@ -242,12 +248,25 @@ func (cfg *Config) load() gperr.Error {
err := model.ACL.Start(cfg.task)
if err != nil {
errs.Add(err)
} else {
logging.Info().Msg("ACL started")
}
}
if errs.HasError() {
notif.Notify(&notif.LogMessage{
Level: zerolog.ErrorLevel,
Title: "Config Reload Error",
Body: notif.ErrorBody{Error: errs.Error()},
})
return errs.Error()
}
return nil
}
func (cfg *Config) initMaxMind(maxmindCfg *maxmind.Config) gperr.Error {
if maxmindCfg != nil {
return maxmind.SetInstance(cfg.task, maxmindCfg)
}
return nil
}
func (cfg *Config) initNotification(notifCfg []notif.NotificationConfig) {
@ -278,6 +297,17 @@ func (cfg *Config) initAutoCert(autocertCfg *autocert.Config) gperr.Error {
return nil
}
func (cfg *Config) initProxmox(proxmoxCfg []proxmox.Config) gperr.Error {
proxmox.Clients.Clear()
errs := gperr.NewBuilder()
for _, cfg := range proxmoxCfg {
if err := cfg.Init(); err != nil {
errs.Add(err.Subject(cfg.URL))
}
}
return errs.Error()
}
func (cfg *Config) errIfExists(p *proxy.Provider) gperr.Error {
if _, ok := cfg.providers.Load(p.String()); ok {
return gperr.Errorf("provider %s already exists", p.String())
@ -296,8 +326,8 @@ func (cfg *Config) loadRouteProviders(providers *config.Providers) gperr.Error {
removeAllAgents()
for _, agent := range providers.Agents {
if err := agent.Start(cfg.task); err != nil {
errs.Add(err.Subject(agent.String()))
if err := agent.Start(cfg.task.Context()); err != nil {
errs.Add(gperr.PrependSubject(agent.String(), err))
continue
}
addAgent(agent)

View file

@ -1,6 +1,7 @@
package config
import (
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/route"
"github.com/yusing/go-proxy/internal/route/provider"
)
@ -23,10 +24,13 @@ func (cfg *Config) DumpRouteProviders() map[string]*provider.Provider {
return entries
}
func (cfg *Config) RouteProviderList() []string {
var list []string
func (cfg *Config) RouteProviderList() []config.RouteProviderListResponse {
var list []config.RouteProviderListResponse
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
list = append(list, p.ShortName())
list = append(list, config.RouteProviderListResponse{
ShortName: p.ShortName(),
FullName: p.String(),
})
})
return list
}

View file

@ -11,7 +11,9 @@ import (
"github.com/yusing/go-proxy/internal/autocert"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging/accesslog"
maxmind "github.com/yusing/go-proxy/internal/maxmind/types"
"github.com/yusing/go-proxy/internal/notif"
"github.com/yusing/go-proxy/internal/proxmox"
"github.com/yusing/go-proxy/internal/utils"
)
@ -30,6 +32,8 @@ type (
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"`
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
Notification []notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"`
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
}
Entrypoint struct {
Middlewares []map[string]any `json:"middlewares"`
@ -38,12 +42,15 @@ type (
HomepageConfig struct {
UseDefaultCategories bool `json:"use_default_categories"`
}
RouteProviderListResponse struct {
ShortName string `json:"short_name"`
FullName string `json:"full_name"`
}
ConfigInstance interface {
Value() *Config
Reload() gperr.Error
Statistics() map[string]any
RouteProviderList() []string
RouteProviderList() []RouteProviderListResponse
Context() context.Context
GetAgent(agentAddrOrDockerHost string) (*agent.AgentConfig, bool)
VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error)
@ -86,7 +93,7 @@ func HasInstance() bool {
func Validate(data []byte) gperr.Error {
var model Config
return utils.DeserializeYAML(data, &model)
return utils.UnmarshalValidateYAML(data, &model)
}
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)

View file

@ -1,6 +1,6 @@
module github.com/yusing/go-proxy/internal/dnsproviders
go 1.24.2
go 1.24.3
replace github.com/yusing/go-proxy => ../..
@ -12,10 +12,10 @@ require (
require (
cloud.google.com/go/auth v0.16.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
@ -23,7 +23,7 @@ require (
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.106 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
@ -39,15 +39,15 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
github.com/aws/smithy-go v1.22.3 // indirect
github.com/baidubce/bce-sdk-go v0.9.224 // indirect
github.com/baidubce/bce-sdk-go v0.9.226 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/boombuler/barcode v1.0.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/civo/civogo v0.3.98 // indirect
github.com/civo/civogo v0.5.0 // indirect
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
github.com/exoscale/egoscale/v3 v3.1.14 // indirect
github.com/exoscale/egoscale/v3 v3.1.17 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
@ -62,7 +62,6 @@ require (
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-resty/resty/v2 v2.16.5 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.17.1 // indirect
github.com/gofrs/flock v0.12.1 // indirect
@ -72,15 +71,16 @@ require (
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/gophercloud/gophercloud v1.14.1 // indirect
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
github.com/gotify/server/v2 v2.6.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.146 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.149 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/infobloxopen/infoblox-go-client/v2 v2.9.0 // indirect
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
@ -89,12 +89,12 @@ require (
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/linode/linodego v1.49.0 // indirect
github.com/linode/linodego v1.50.0 // indirect
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.65 // indirect
github.com/miekg/dns v1.1.66 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -113,7 +113,7 @@ require (
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/oracle/oci-go-sdk/v65 v65.89.2 // indirect
github.com/oracle/oci-go-sdk/v65 v65.91.0 // indirect
github.com/ovh/go-ovh v1.7.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
@ -122,12 +122,12 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/otp v1.4.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/puzpuzpuz/xsync/v4 v4.1.0 // indirect
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/sacloud/api-client-go v0.2.10 // indirect
github.com/sacloud/go-http v0.1.9 // indirect
github.com/sacloud/iaas-api-go v1.14.0 // indirect
github.com/sacloud/iaas-api-go v1.15.0 // indirect
github.com/sacloud/packages-go v0.0.11 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 // indirect
@ -141,19 +141,19 @@ require (
github.com/sony/gobreaker v1.0.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cast v1.8.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1150 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1164 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/transip/gotransip/v6 v6.26.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/volcengine/volc-sdk-golang v1.0.205 // indirect
github.com/vultr/govultr/v3 v3.19.1 // indirect
github.com/volcengine/volc-sdk-golang v1.0.207 // indirect
github.com/vultr/govultr/v3 v3.20.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver v1.17.3 // indirect
@ -165,28 +165,28 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.32.0 // indirect
google.golang.org/api v0.230.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect
google.golang.org/grpc v1.72.0 // indirect
golang.org/x/tools v0.33.0 // indirect
google.golang.org/api v0.233.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.14.2 // indirect
gopkg.in/ns1/ns1-go.v2 v2.14.3 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.33.0 // indirect
k8s.io/apimachinery v0.33.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect

View file

@ -179,8 +179,8 @@ cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZ
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=
cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w=
@ -606,8 +606,8 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesn
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 h1:j8BorDEigD8UFOSZQiSqAMOOleyQOOQPnUAwV+Ls1gA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
@ -654,8 +654,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.106 h1:+YPfQheppCKOPJxhWDmStF1UMJrxnA1iiwBH12t6Fa4=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.106/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
@ -702,8 +702,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjK
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/baidubce/bce-sdk-go v0.9.224 h1:z2L8alGw/y3IUHjrLRyrxrgCvMssYTjgCd7OQdb4gt0=
github.com/baidubce/bce-sdk-go v0.9.224/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/baidubce/bce-sdk-go v0.9.226 h1:VKEKcJC9P33yIfYJZr12Q/4Bvj18RFbgO8w8XOfU8AI=
github.com/baidubce/bce-sdk-go v0.9.226/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@ -717,10 +717,6 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBW
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY=
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
@ -743,14 +739,12 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/civo/civogo v0.3.98 h1:FEbB5oxCcHeHUK3fJODxVoMQzhpLV9Jtb7bezANTY5c=
github.com/civo/civogo v0.3.98/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
github.com/civo/civogo v0.5.0 h1:YDG38z+hmgaAhpDad1n/9sZrBtwRTeKVvMgdrS8Gwy4=
github.com/civo/civogo v0.5.0/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM=
github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@ -807,8 +801,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/exoscale/egoscale/v3 v3.1.14 h1:ux1wOtx4561ZJM1sF2AFEjEY6HRj/RbtglKvZxh2iqg=
github.com/exoscale/egoscale/v3 v3.1.14/go.mod h1:t9+MpSEam94na48O/xgvvPFpQPRiwZ3kBN4/UuQtKco=
github.com/exoscale/egoscale/v3 v3.1.17 h1:+T6+GP/k3tFNsYIQzpF3Sou4ecH//FkERDsJze/OZ00=
github.com/exoscale/egoscale/v3 v3.1.17/go.mod h1:t9+MpSEam94na48O/xgvvPFpQPRiwZ3kBN4/UuQtKco=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@ -893,8 +887,6 @@ github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlnd
github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
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=
@ -1000,8 +992,8 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
@ -1030,8 +1022,8 @@ github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMd
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
@ -1047,6 +1039,8 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I=
github.com/gotify/server/v2 v2.6.3/go.mod h1:IyeQ/iL3vetcuqUAzkCMVObIMGGJx4zb13/mVatIwE8=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
@ -1106,8 +1100,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.146 h1:ld5s5UeA9zgyFsZskVD2Tr6k6VnJWkvaLm5nqvfOEf4=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.146/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.149 h1:gDzo/eYE8/mwF5fi7v10pdBW2027PUvaHvRPA5aOPoM=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.149/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -1117,11 +1111,11 @@ github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhK
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/infobloxopen/infoblox-go-client/v2 v2.9.0 h1:wS8kTlQVeVbrepeY83s9X+XdSa6Qah5KO+tdW+zRQXU=
github.com/infobloxopen/infoblox-go-client/v2 v2.9.0/go.mod h1:NeNJpz09efw/edzqkVivGv1bWqBXTomqYBRFbP+XBqg=
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 h1:AKsihjFT/t6Y0keEv3p59DACcOuh0inWXdUB0ZOzYH0=
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0/go.mod h1:NeNJpz09efw/edzqkVivGv1bWqBXTomqYBRFbP+XBqg=
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k=
github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
@ -1163,8 +1157,6 @@ github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -1189,8 +1181,8 @@ github.com/labbsr0x/goh v1.0.1 h1:97aBJkDjpyBZGPbQuOK5/gHcSFbcr5aRsq3RSRJFpPk=
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
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/linode/linodego v1.49.0 h1:MNd3qwvQzbXB5mCpvdCqlUIu1RPA9oC+50LyB9kK+GQ=
github.com/linode/linodego v1.49.0/go.mod h1:B+HAM3//4w1wOS0BwdaQBKwBxlfe6kYJ7bSC6jJ/xtc=
github.com/linode/linodego v1.50.0 h1:5y79VvvQnWb5JyPIjTwyUrU3ArHcs7XZQFdkPS/lNpw=
github.com/linode/linodego v1.50.0/go.mod h1:9S+REoPCtUNWCm63D1vjjxIJZfwEL2t2kTDnwt620FM=
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
@ -1236,8 +1228,8 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34=
github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
@ -1330,8 +1322,8 @@ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYr
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
github.com/oracle/oci-go-sdk/v65 v65.89.2 h1:w0GwID9NlT+eg3InbAwkWsazDtxVLYQ8rJb4E33Yb14=
github.com/oracle/oci-go-sdk/v65 v65.89.2/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
github.com/oracle/oci-go-sdk/v65 v65.91.0 h1:maO6AxKxVfszH0X4tbbtN21jOk03lCRR3IqiA8/FzZc=
github.com/oracle/oci-go-sdk/v65 v65.91.0/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA=
github.com/ovh/go-ovh v1.7.0 h1:V14nF7FwDjQrZt9g7jzcvAAQ3HN6DNShRFRMC3jLoPw=
github.com/ovh/go-ovh v1.7.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@ -1401,12 +1393,12 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/regfish/regfish-dnsapi-go v0.1.1 h1:TJFtbePHkd47q5GZwYl1h3DIYXmoxdLjW/SBsPtB5IE=
github.com/regfish/regfish-dnsapi-go v0.1.1/go.mod h1:ubIgXSfqarSnl3XHSn8hIFwFF3h0yrq0ZiWD93Y2VjY=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@ -1430,8 +1422,8 @@ github.com/sacloud/api-client-go v0.2.10 h1:+rv3jDohD+pkdYwOTBiB+jZsM0xK3AxadXRz
github.com/sacloud/api-client-go v0.2.10/go.mod h1:Jj3CTy2+O4bcMedVDXlbHuqqche85HEPuVXoQFhLaRc=
github.com/sacloud/go-http v0.1.9 h1:Xa5PY8/pb7XWhwG9nAeXSrYXPbtfBWqawgzxD5co3VE=
github.com/sacloud/go-http v0.1.9/go.mod h1:DpDG+MSyxYaBwPJ7l3aKLMzwYdTVtC5Bo63HActcgoE=
github.com/sacloud/iaas-api-go v1.14.0 h1:xjkFWqdo4ilTrKPNNYBNWR/CZ/kVRsJrdAHAad6J/AQ=
github.com/sacloud/iaas-api-go v1.14.0/go.mod h1:C8os2Mnj0TOmMdSllwhaDWKMVG2ysFnpe69kyA4M3V0=
github.com/sacloud/iaas-api-go v1.15.0 h1:G88U69OOVUSjOZ2fEP5QWMg7r3VOVsfNkl5Mxl+/wMk=
github.com/sacloud/iaas-api-go v1.15.0/go.mod h1:AU6TM3giGEeyl/p1FAYfpwMDpkl7aLco2svdYXnrAfA=
github.com/sacloud/packages-go v0.0.11 h1:hrRWLmfPM9w7GBs6xb5/ue6pEMl8t1UuDKyR/KfteHo=
github.com/sacloud/packages-go v0.0.11/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZOmmQORIzcurJ8=
github.com/sagikazarmark/crypt v0.10.0/go.mod h1:gwTNHQVoOS3xp9Xvz5LLR+1AauC5M6880z5NWzdhOyQ=
@ -1484,8 +1476,8 @@ github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZ
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
@ -1527,8 +1519,8 @@ github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNG
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1136/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1150 h1:r/cHvpMZ0oO5/HOuSsPdq3Dj1YX4pF0mhZS7G5gWKEs=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1150/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1164 h1:qEzZCZf1sgvvrZ8ngws0gZlyW+sOdY0K9VXGm4AcvTE=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1164/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 h1:kMIdSU5IvpOROh27ToVQ3hlm6ym3lCRs9tnGCOBoZqk=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136/go.mod h1:FpyIz3mymKaExVs6Fz27kxDBS42jqZn7vbACtxdeEH4=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
@ -1537,8 +1529,6 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1
github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550=
github.com/transip/gotransip/v6 v6.26.0/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
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/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
@ -1548,10 +1538,10 @@ github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
github.com/volcengine/volc-sdk-golang v1.0.205 h1:0ukVBX82JaF9N5H/D8j8jPjgJW52bQpAXabrg4WLJ88=
github.com/volcengine/volc-sdk-golang v1.0.205/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
github.com/vultr/govultr/v3 v3.19.1 h1:31rOP5Tz40AOc8h6Ws4ryzqAniUBffgRhy9uMG/EFvs=
github.com/vultr/govultr/v3 v3.19.1/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=
github.com/volcengine/volc-sdk-golang v1.0.207 h1:1OJ/nC92dF1URRoyO1AHSghCob12NT1PAA/GoK8uU18=
github.com/volcengine/volc-sdk-golang v1.0.207/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
github.com/vultr/govultr/v3 v3.20.0 h1:O+Om6gXpN6ehwAIIKq5DyGuekpyHaoRlwrxTb44bDzA=
github.com/vultr/govultr/v3 v3.20.0/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@ -1635,8 +1625,6 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -1667,8 +1655,8 @@ golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIi
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.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1806,8 +1794,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1837,8 +1825,8 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1855,8 +1843,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1973,8 +1961,8 @@ golang.org/x/sys v0.10.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.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -1989,8 +1977,8 @@ golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
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.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -2010,8 +1998,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -2091,8 +2079,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -2167,8 +2155,8 @@ google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60c
google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM=
google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ=
google.golang.org/api v0.233.0 h1:iGZfjXAJiUFSSaekVB7LzXl6tRfEKhUN7FkZN++07tI=
google.golang.org/api v0.233.0/go.mod h1:TCIVLLlcwunlMpZIhIp7Ltk77W+vUSdUKAAIlbxY44c=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -2309,11 +2297,12 @@ google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOl
google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU=
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f h1:tjZsroqekhC63+WMqzmWyW5Twj/ZfR5HAlpd5YQ1Vs0=
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f h1:N/PrbTw4kdkqNRzVfWPrBekzLuarFREcbFOiOLkXon4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -2356,8 +2345,8 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@ -2398,8 +2387,8 @@ gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/ns1/ns1-go.v2 v2.14.2 h1:wz/toj9U20wBrmYxW4vTz7sZWED+JJVRjUBBJ7CKrzI=
gopkg.in/ns1/ns1-go.v2 v2.14.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
gopkg.in/ns1/ns1-go.v2 v2.14.3 h1:Yn72GgB6AA9I4602AsLMtbC1ZKT5EUrKiG+IPS+Ovr0=
gopkg.in/ns1/ns1-go.v2 v2.14.3/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
@ -2433,8 +2422,8 @@ k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg=
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=

View file

@ -1,24 +1,26 @@
package docker
import (
"context"
"net"
"net/url"
"strconv"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
"github.com/yusing/go-proxy/agent/pkg/agent"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/gperr"
idlewatcher "github.com/yusing/go-proxy/internal/idlewatcher/types"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/utils"
U "github.com/yusing/go-proxy/internal/utils"
)
type (
PortMapping = map[int]container.Port
Container struct {
_ U.NoCopy
_ utils.NoCopy
DockerHost string `json:"docker_host"`
Image *ContainerImage `json:"image"`
@ -40,6 +42,7 @@ type (
Aliases []string `json:"aliases"`
IsExcluded bool `json:"is_excluded"`
IsExplicit bool `json:"is_explicit"`
IsHostNetworkMode bool `json:"is_host_network_mode"`
Running bool `json:"running"`
}
ContainerImage struct {
@ -79,6 +82,7 @@ func FromDocker(c *container.SummaryTrimmed, dockerHost string) (res *Container)
Aliases: helper.getAliases(),
IsExcluded: isExcluded,
IsExplicit: isExplicit,
IsHostNetworkMode: c.HostConfig.NetworkMode == "host",
Running: c.Status == "running" || c.State == "running",
}
@ -100,6 +104,33 @@ func (c *Container) IsBlacklisted() bool {
return c.Image.IsBlacklisted() || c.isDatabase()
}
func (c *Container) UpdatePorts() error {
client, err := NewClient(c.DockerHost)
if err != nil {
return err
}
defer client.Close()
inspect, err := client.ContainerInspect(context.Background(), c.ContainerID)
if err != nil {
return err
}
for port := range inspect.Config.ExposedPorts {
proto, portStr := nat.SplitProtoPort(string(port))
portInt, _ := nat.ParsePort(portStr)
if portInt == 0 {
continue
}
c.PublicPortMapping[portInt] = container.Port{
PublicPort: uint16(portInt),
PrivatePort: uint16(portInt),
Type: proto,
}
}
return nil
}
var databaseMPs = map[string]struct{}{
"/var/lib/postgresql/data": {},
"/var/lib/mysql": {},
@ -126,11 +157,27 @@ func (c *Container) isDatabase() bool {
return false
}
func (c *Container) isLocal() bool {
if strings.HasPrefix(c.DockerHost, "unix://") {
return true
}
url, err := url.Parse(c.DockerHost)
if err != nil {
return false
}
hostname := url.Hostname()
ip := net.ParseIP(hostname)
if ip != nil {
return ip.IsLoopback() || ip.IsUnspecified()
}
return hostname == "localhost"
}
func (c *Container) setPublicHostname() {
if !c.Running {
return
}
if strings.HasPrefix(c.DockerHost, "unix://") {
if c.isLocal() {
c.PublicHostname = "127.0.0.1"
return
}
@ -144,19 +191,18 @@ func (c *Container) setPublicHostname() {
}
func (c *Container) setPrivateHostname(helper containerHelper) {
if !strings.HasPrefix(c.DockerHost, "unix://") && c.Agent == nil {
if !c.isLocal() && c.Agent == nil {
return
}
if helper.NetworkSettings == nil {
return
}
for _, v := range helper.NetworkSettings.Networks {
if v.IPAddress == "" {
continue
}
if v.IPAddress != "" {
c.PrivateHostname = v.IPAddress
return
}
}
}
func (c *Container) loadDeleteIdlewatcherLabels(helper containerHelper) {
@ -178,7 +224,7 @@ func (c *Container) loadDeleteIdlewatcherLabels(helper containerHelper) {
ContainerName: c.ContainerName,
},
}
err := utils.Deserialize(cfg, idwCfg)
err := utils.MapUnmarshalValidate(cfg, idwCfg)
if err != nil {
gperr.LogWarn("invalid idlewatcher config", gperr.PrependSubject(c.ContainerName, err))
} else {

View file

@ -41,3 +41,38 @@ func TestContainerExplicit(t *testing.T) {
})
}
}
func TestContainerHostNetworkMode(t *testing.T) {
tests := []struct {
name string
container *container.SummaryTrimmed
isHostNetworkMode bool
}{
{
name: "host network mode",
container: &container.SummaryTrimmed{
Names: []string{"test"},
State: "test",
HostConfig: struct {
NetworkMode string `json:",omitempty"`
}{
NetworkMode: "host",
},
},
isHostNetworkMode: true,
},
{
name: "not host network mode",
container: &container.SummaryTrimmed{
Names: []string{"test"},
State: "test",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := FromDocker(tt.container, "")
ExpectEqual(t, c.IsHostNetworkMode, tt.isHostNetworkMode)
})
}
}

View file

@ -78,7 +78,7 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
})
}
if ep.middleware != nil {
ep.middleware.ServeHTTP(mux.ServeHTTP, w, r)
ep.middleware.ServeHTTP(mux.ServeHTTP, w, routes.WithRouteContext(r, mux))
return
}
mux.ServeHTTP(w, r)

View file

@ -36,8 +36,11 @@ func (err *baseError) Subjectf(format string, args ...any) Error {
return err.Subject(format)
}
func (err baseError) With(extra error) Error {
return &nestedError{&err, []error{extra}}
func (err *baseError) With(extra error) Error {
if extra == nil {
return err
}
return &nestedError{&baseError{err.Err}, []error{extra}}
}
func (err baseError) Withf(format string, args ...any) Error {
@ -62,3 +65,11 @@ func (err *baseError) MarshalJSON() ([]byte, error) {
return json.Marshal(err.Error())
}
}
func (err *baseError) Plain() []byte {
return Plain(err.Err)
}
func (err *baseError) Markdown() []byte {
return Markdown(err.Err)
}

View file

@ -24,10 +24,6 @@ type Builder struct {
rwLock
}
type multiline struct {
*Builder
}
// NewBuilder creates a new Builder.
//
// If about is not provided, the Builder will not have a subject
@ -63,6 +59,9 @@ func (b *Builder) Error() Error {
if len(b.errs) == 0 {
return nil
}
if len(b.errs) == 1 && b.about == "" {
return wrap(b.errs[0])
}
return &nestedError{Err: New(b.about), Extras: b.errs}
}
@ -88,23 +87,6 @@ func (b *Builder) Add(err error) {
b.add(err)
}
func (b *Builder) add(err error) {
switch err := err.(type) {
case *baseError:
b.errs = append(b.errs, err.Err)
case *nestedError:
if err.Err == nil {
b.errs = append(b.errs, err.Extras...)
} else {
b.errs = append(b.errs, err)
}
case *MultilineError:
b.add(&err.nestedError)
default:
b.errs = append(b.errs, err)
}
}
func (b *Builder) Adds(err string) {
b.Lock()
defer b.Unlock()
@ -160,3 +142,20 @@ func (b *Builder) ForEach(fn func(error)) {
fn(err)
}
}
func (b *Builder) add(err error) {
switch err := err.(type) { //nolint:errorlint
case *baseError:
b.errs = append(b.errs, err.Err)
case *nestedError:
if err.Err == nil {
b.errs = append(b.errs, err.Extras...)
} else {
b.errs = append(b.errs, err)
}
case *MultilineError:
b.add(&err.nestedError)
default:
b.errs = append(b.errs, err)
}
}

View file

@ -50,6 +50,7 @@ func TestBuilderNested(t *testing.T) {
Inner: 1
Inner: 2
Action 2
Inner: 3`
Inner: 3
`
ExpectEqual(t, got, expected)
}

View file

@ -20,6 +20,16 @@ type Error interface {
Subject(subject string) Error
// Subjectf is a wrapper for Subject(fmt.Sprintf(format, args...)).
Subjectf(format string, args ...any) Error
PlainError
MarkdownError
}
type PlainError interface {
Plain() []byte
}
type MarkdownError interface {
Markdown() []byte
}
// this makes JSON marshaling work,

View file

@ -153,6 +153,7 @@ func TestErrorStringNested(t *testing.T) {
2
action 3 > inner3: generic failure
3
3`
3
`
expect.Equal(t, ansi.StripANSI(ne.Error()), want)
}

43
internal/gperr/hint.go Normal file
View file

@ -0,0 +1,43 @@
package gperr
import "github.com/yusing/go-proxy/internal/utils/strutils/ansi"
type Hint struct {
Prefix string
Message string
Suffix string
}
var _ PlainError = (*Hint)(nil)
var _ MarkdownError = (*Hint)(nil)
func (h *Hint) Error() string {
return h.Prefix + ansi.Info(h.Message) + h.Suffix
}
func (h *Hint) Plain() []byte {
return []byte(h.Prefix + h.Message + h.Suffix)
}
func (h *Hint) Markdown() []byte {
return []byte(h.Prefix + "**" + h.Message + "**" + h.Suffix)
}
func (h *Hint) MarshalText() ([]byte, error) {
return h.Plain(), nil
}
func (h *Hint) String() string {
return h.Error()
}
func DoYouMean(s string) error {
if s == "" {
return nil
}
return &Hint{
Prefix: "Do you mean ",
Message: s,
Suffix: "?",
}
}

View file

@ -15,7 +15,7 @@ func log(msg string, err error, level zerolog.Level, logger ...*zerolog.Logger)
} else {
l = logging.GetLogger()
}
l.WithLevel(level).Msg(New(highlight(msg)).With(err).Error())
l.WithLevel(level).Msg(New(highlightANSI(msg)).With(err).Error())
switch level {
case zerolog.FatalLevel:
os.Exit(1)

View file

@ -14,6 +14,9 @@ func Multiline() *MultilineError {
}
func (m *MultilineError) add(err error) {
if err == nil {
return
}
m.Extras = append(m.Extras, err)
}

View file

@ -3,8 +3,6 @@ package gperr
import (
"errors"
"fmt"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
//nolint:recvcheck
@ -13,9 +11,11 @@ type nestedError struct {
Extras []error `json:"extras"`
}
var emptyError = errStr("")
func (err nestedError) Subject(subject string) Error {
if err.Err == nil {
err.Err = PrependSubject(subject, errStr(""))
err.Err = PrependSubject(subject, emptyError)
} else {
err.Err = PrependSubject(subject, err.Err)
}
@ -67,48 +67,86 @@ func (err *nestedError) Is(other error) bool {
return false
}
func (err *nestedError) Error() string {
var nilError = newError("<nil>")
var bulletPrefix = []byte("• ")
var markdownBulletPrefix = []byte("- ")
var spaces = []byte(" ")
type appendLineFunc func(buf []byte, err error, level int) []byte
func (err *nestedError) fmtError(appendLine appendLineFunc) []byte {
if err == nil {
return makeLine("<nil>", 0)
return appendLine(nil, nilError, 0)
}
if err.Err != nil {
lines := make([]string, 0, 1+len(err.Extras))
lines = append(lines, makeLine(err.Err.Error(), 0))
lines = append(lines, makeLines(err.Extras, 1)...)
return strutils.JoinLines(lines)
buf := appendLine(nil, err.Err, 0)
if len(err.Extras) > 0 {
buf = append(buf, '\n')
buf = appendLines(buf, err.Extras, 1, appendLine)
}
return strutils.JoinLines(makeLines(err.Extras, 0))
return buf
}
return appendLines(nil, err.Extras, 0, appendLine)
}
//go:inline
func makeLine(err string, level int) string {
const bulletPrefix = "• "
const spaces = " "
func (err *nestedError) Error() string {
return string(err.fmtError(appendLineNormal))
}
func (err *nestedError) Plain() []byte {
return err.fmtError(appendLinePlain)
}
func (err *nestedError) Markdown() []byte {
return err.fmtError(appendLineMd)
}
func appendLine(buf []byte, err error, level int, prefix []byte, format func(err error) []byte) []byte {
if err == nil {
return appendLine(buf, nilError, level, prefix, format)
}
if level == 0 {
return err
return append(buf, format(err)...)
}
return spaces[:2*level] + bulletPrefix + err
buf = append(buf, spaces[:2*level]...)
buf = append(buf, prefix...)
buf = append(buf, format(err)...)
return buf
}
func makeLines(errs []error, level int) []string {
func appendLineNormal(buf []byte, err error, level int) []byte {
return appendLine(buf, err, level, bulletPrefix, Normal)
}
func appendLinePlain(buf []byte, err error, level int) []byte {
return appendLine(buf, err, level, bulletPrefix, Plain)
}
func appendLineMd(buf []byte, err error, level int) []byte {
return appendLine(buf, err, level, markdownBulletPrefix, Markdown)
}
func appendLines(buf []byte, errs []error, level int, appendLine appendLineFunc) []byte {
if len(errs) == 0 {
return nil
return buf
}
lines := make([]string, 0, len(errs))
for _, err := range errs {
switch err := wrap(err).(type) {
case *nestedError:
if err.Err != nil {
lines = append(lines, makeLine(err.Err.Error(), level))
lines = append(lines, makeLines(err.Extras, level+1)...)
buf = appendLine(buf, err.Err, level)
buf = append(buf, '\n')
buf = appendLines(buf, err.Extras, level+1, appendLine)
} else {
lines = append(lines, makeLines(err.Extras, level)...)
buf = appendLines(buf, err.Extras, level, appendLine)
}
default:
lines = append(lines, makeLine(err.Error(), level))
if err == nil {
continue
}
buf = appendLine(buf, err, level)
buf = append(buf, '\n')
}
}
return lines
return buf
}

View file

@ -1,10 +1,10 @@
package gperr
import (
"bytes"
"encoding/json"
"errors"
"slices"
"strings"
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
)
@ -19,10 +19,23 @@ type withSubject struct {
const subjectSep = " > "
func highlight(subject string) string {
type highlightFunc func(subject string) string
var _ PlainError = (*withSubject)(nil)
var _ MarkdownError = (*withSubject)(nil)
func highlightANSI(subject string) string {
return ansi.HighlightRed + subject + ansi.Reset
}
func highlightMarkdown(subject string) string {
return "**" + subject + "**"
}
func noHighlight(subject string) string {
return subject
}
func PrependSubject(subject string, err error) error {
if err == nil {
return nil
@ -32,6 +45,11 @@ func PrependSubject(subject string, err error) error {
switch err := err.(type) {
case *withSubject:
return err.Prepend(subject)
case *wrappedError:
return &wrappedError{
Err: PrependSubject(subject, err.Err),
Message: err.Message,
}
case Error:
return err.Subject(subject)
}
@ -69,24 +87,42 @@ func (err *withSubject) Unwrap() error {
}
func (err *withSubject) Error() string {
return string(err.fmtError(highlightANSI))
}
func (err *withSubject) Plain() []byte {
return err.fmtError(noHighlight)
}
func (err *withSubject) Markdown() []byte {
return err.fmtError(highlightMarkdown)
}
func (err *withSubject) fmtError(highlight highlightFunc) []byte {
// subject is in reversed order
n := len(err.Subjects)
size := 0
errStr := err.Err.Error()
var sb strings.Builder
for _, s := range err.Subjects {
subjects := err.Subjects
if err.pendingSubject != "" {
subjects = append(subjects, err.pendingSubject)
}
var buf bytes.Buffer
for _, s := range subjects {
size += len(s)
}
sb.Grow(size + 2 + n*len(subjectSep) + len(errStr) + len(highlight("")))
n := len(subjects)
buf.Grow(size + 2 + n*len(subjectSep) + len(errStr) + len(highlight("")))
for i := n - 1; i > 0; i-- {
sb.WriteString(err.Subjects[i])
sb.WriteString(subjectSep)
buf.WriteString(subjects[i])
buf.WriteString(subjectSep)
}
sb.WriteString(highlight(err.Subjects[0]))
sb.WriteString(": ")
sb.WriteString(errStr)
return sb.String()
buf.WriteString(highlight(subjects[0]))
if errStr != "" {
buf.WriteString(": ")
buf.WriteString(errStr)
}
return buf.Bytes()
}
// MarshalJSON implements the json.Marshaler interface.
@ -100,6 +136,9 @@ func (err *withSubject) MarshalJSON() ([]byte, error) {
Subjects: subjects,
Err: err.Err,
}
if err.pendingSubject != "" {
reversed.Subjects = append(reversed.Subjects, err.pendingSubject)
}
return json.Marshal(reversed)
}

View file

@ -1,8 +1,6 @@
package gperr
import (
"encoding/json"
"errors"
"fmt"
)
@ -32,13 +30,13 @@ func Wrap(err error, message ...string) Error {
//nolint:errorlint
switch err := err.(type) {
case *baseError:
err.Err = fmt.Errorf("%s: %w", message[0], err.Err)
err.Err = &wrappedError{err.Err, message[0]}
return err
case *nestedError:
err.Err = fmt.Errorf("%s: %w", message[0], err.Err)
err.Err = &wrappedError{err.Err, message[0]}
return err
}
return &baseError{fmt.Errorf("%s: %w", message[0], err)}
return &baseError{&wrappedError{err, message[0]}}
}
func Unwrap(err error) Error {
@ -65,18 +63,6 @@ func wrap(err error) Error {
return &baseError{err}
}
func IsJSONMarshallable(err error) bool {
switch err := err.(type) {
case *nestedError, *withSubject:
return true
case *baseError:
return IsJSONMarshallable(err.Err)
default:
var v json.Marshaler
return errors.As(err, &v)
}
}
func Join(errors ...error) Error {
n := 0
for _, err := range errors {
@ -98,8 +84,50 @@ func Join(errors ...error) Error {
return &nestedError{Extras: errs}
}
func JoinLines(main error, errors ...string) Error {
errs := make([]error, len(errors))
for i, err := range errors {
if err == "" {
continue
}
errs[i] = newError(err)
}
return &nestedError{Err: main, Extras: errs}
}
func Collect[T any, Err error, Arg any, Func func(Arg) (T, Err)](eb *Builder, fn Func, arg Arg) T {
result, err := fn(arg)
eb.Add(err)
return result
}
func Normal(err error) []byte {
if err == nil {
return nil
}
return []byte(err.Error())
}
func Plain(err error) []byte {
if err == nil {
return nil
}
if p, ok := err.(PlainError); ok {
return p.Plain()
}
return []byte(err.Error())
}
func Markdown(err error) []byte {
if err == nil {
return nil
}
switch err := err.(type) {
case MarkdownError:
return err.Markdown()
case interface{ Unwrap() []error }:
return appendLines(nil, err.Unwrap(), 0, appendLineMd)
default:
return []byte(err.Error())
}
}

View file

@ -1,63 +1,55 @@
package gperr
import (
"errors"
"testing"
)
type testErr struct{}
func (e *testErr) Error() string {
func (e testErr) Error() string {
return "test error"
}
func (e *testErr) MarshalJSON() ([]byte, error) {
return nil, nil
func (e testErr) Plain() []byte {
return []byte("test error")
}
func TestIsJSONMarshallable(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{
name: "testErr",
err: &testErr{},
want: true,
},
{
name: "baseError",
err: &baseError{},
want: false,
},
{
name: "baseError with json marshallable error",
err: &baseError{&testErr{}},
want: true,
},
{
name: "nestedError",
err: &nestedError{},
want: true,
},
{
name: "withSubject",
err: &withSubject{},
want: true,
},
{
name: "standard error",
err: errors.New("test error"),
want: false,
},
}
func (e testErr) Markdown() []byte {
return []byte("**test error**")
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := IsJSONMarshallable(test.err); got != test.want {
t.Errorf("IsJSONMarshallable(%v) = %v, want %v", test.err, got, test.want)
type testMultiErr struct {
errors []error
}
func (e testMultiErr) Error() string {
return Join(e.errors...).Error()
}
func (e testMultiErr) Unwrap() []error {
return e.errors
}
func TestFormatting(t *testing.T) {
err := testErr{}
plain := Plain(err)
if string(plain) != "test error" {
t.Errorf("expected test error, got %s", string(plain))
}
})
md := Markdown(err)
if string(md) != "**test error**" {
t.Errorf("expected test error, got %s", string(md))
}
}
func TestMultiError(t *testing.T) {
err := testMultiErr{[]error{testErr{}, testErr{}}}
plain := Plain(err)
if string(plain) != "test error\ntest error\n" {
t.Errorf("expected test error, got %s", string(plain))
}
md := Markdown(err)
if string(md) != "**test error**\n**test error**\n" {
t.Errorf("expected test error, got %s", string(md))
}
}

34
internal/gperr/wrapped.go Normal file
View file

@ -0,0 +1,34 @@
package gperr
import (
"errors"
"fmt"
)
type wrappedError struct {
Err error
Message string
}
var _ PlainError = (*wrappedError)(nil)
var _ MarkdownError = (*wrappedError)(nil)
func (e *wrappedError) Error() string {
return fmt.Sprintf("%s: %s", e.Message, e.Err.Error())
}
func (e *wrappedError) Plain() []byte {
return fmt.Appendf(nil, "%s: %s", e.Message, e.Err.Error())
}
func (e *wrappedError) Markdown() []byte {
return fmt.Appendf(nil, "**%s**: %s", e.Message, e.Err.Error())
}
func (e *wrappedError) Unwrap() error {
return e.Err
}
func (e *wrappedError) Is(target error) bool {
return errors.Is(e.Err, target)
}

View file

@ -60,7 +60,7 @@ func fetchIconAbsolute(ctx context.Context, url string) *FetchResult {
return result
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "request timeout"}
@ -114,12 +114,18 @@ func fetchKnownIcon(ctx context.Context, url *IconURL) *FetchResult {
return fetchIconAbsolute(ctx, url.URL())
}
func fetchIcon(ctx context.Context, filetype, filename string) *FetchResult {
result := fetchKnownIcon(ctx, NewSelfhStIconURL(filename, filetype))
func fetchIcon(ctx context.Context, filename string) *FetchResult {
for _, fileType := range []string{"svg", "webp", "png"} {
result := fetchKnownIcon(ctx, NewSelfhStIconURL(filename, fileType))
if result.OK() {
return result
}
return fetchKnownIcon(ctx, NewWalkXCodeIconURL(filename, filetype))
result = fetchKnownIcon(ctx, NewWalkXCodeIconURL(filename, fileType))
if result.OK() {
return result
}
}
return &FetchResult{StatusCode: http.StatusNotFound, ErrMsg: "no icon found"}
}
func FindIcon(ctx context.Context, r route, uri string) *FetchResult {
@ -127,17 +133,18 @@ func FindIcon(ctx context.Context, r route, uri string) *FetchResult {
return result
}
result := fetchIcon(ctx, "png", sanitizeName(r.Reference()))
if !result.OK() {
if r, ok := r.(httpRoute); ok {
// fallback to parse html
result = findIconSlow(ctx, r, uri, nil)
}
}
for _, ref := range r.References() {
result := fetchIcon(ctx, sanitizeName(ref))
if result.OK() {
storeIconCache(r.Key(), result)
}
return result
}
}
if r, ok := r.(httpRoute); ok {
// fallback to parse html
return findIconSlow(ctx, r, uri, nil)
}
return &FetchResult{StatusCode: http.StatusNotFound, ErrMsg: "no icon found"}
}
func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string) *FetchResult {
@ -154,7 +161,7 @@ func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string)
ctx, cancel := context.WithTimeoutCause(ctx, faviconFetchTimeout, errors.New("favicon request timeout"))
defer cancel()
newReq, err := http.NewRequestWithContext(ctx, "GET", r.TargetURL().String(), nil)
newReq, err := http.NewRequestWithContext(ctx, http.MethodGet, r.TargetURL().String(), nil)
if err != nil {
return &FetchResult{StatusCode: http.StatusInternalServerError, ErrMsg: "cannot create request"}
}

View file

@ -2,9 +2,9 @@ package homepage
import (
"encoding/json"
"strings"
"slices"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/homepage/widgets"
"github.com/yusing/go-proxy/internal/utils"
)
@ -19,14 +19,15 @@ type (
Category string `json:"category"`
Description string `json:"description" aliases:"desc"`
SortOrder int `json:"sort_order"`
WidgetConfig map[string]any `json:"widget_config" aliases:"widget"`
}
Item struct {
*ItemConfig
WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget"`
Alias string
Provider string
OriginURL string
}
)
@ -43,23 +44,10 @@ func (cfg *ItemConfig) GetOverride(alias string) *ItemConfig {
}
func (item *Item) MarshalJSON() ([]byte, error) {
var url *string
if !strings.ContainsRune(item.Alias, '.') {
godoxyCfg := config.GetInstance().Value()
// use first domain as base domain
domains := godoxyCfg.MatchDomains
if len(domains) > 0 {
url = new(string)
*url = item.Alias + domains[0]
}
} else {
url = &item.Alias
}
return json.Marshal(map[string]any{
"show": item.Show,
"alias": item.Alias,
"provider": item.Provider,
"url": url,
"name": item.Name,
"icon": item.Icon,
"category": item.Category,
@ -74,4 +62,13 @@ func (c Homepage) Add(item *Item) {
c[item.Category] = make(Category, 0)
}
c[item.Category] = append(c[item.Category], item)
slices.SortStableFunc(c[item.Category], func(a, b *Item) int {
if a.SortOrder < b.SortOrder {
return -1
}
if a.SortOrder > b.SortOrder {
return 1
}
return 0
})
}

View file

@ -1,8 +1,9 @@
package homepage
package homepage_test
import (
"testing"
. "github.com/yusing/go-proxy/internal/homepage"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
@ -13,7 +14,7 @@ func TestOverrideItem(t *testing.T) {
Show: false,
Name: "Foo",
Icon: &IconURL{
Value: "/favicon.ico",
FullURL: strPtr("/favicon.ico"),
IconSource: IconSourceRelative,
},
Category: "App",
@ -24,7 +25,7 @@ func TestOverrideItem(t *testing.T) {
Name: "Bar",
Category: "Test",
Icon: &IconURL{
Value: "@walkxcode/example.png",
FullURL: strPtr("@walkxcode/example.png"),
IconSource: IconSourceWalkXCode,
},
}

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/jsonstore"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
@ -15,34 +16,24 @@ import (
type cacheEntry struct {
Icon []byte `json:"icon"`
ContentType string `json:"content_type"`
ContentType string `json:"content_type,omitempty"`
LastAccess atomic.Value[time.Time] `json:"last_access"`
}
// cache key can be absolute url or route name.
var (
iconCache = make(map[string]*cacheEntry)
iconCacheMu sync.RWMutex
iconCache = jsonstore.Store[*cacheEntry](common.NamespaceIconCache)
iconMu sync.RWMutex
)
const (
iconCacheTTL = 3 * 24 * time.Hour
cleanUpInterval = time.Minute
maxCacheSize = 1024 * 1024 // 1MB
maxIconSize = 1024 * 1024 // 1MB
maxCacheEntries = 100
)
func InitIconCache() {
iconCacheMu.Lock()
defer iconCacheMu.Unlock()
err := utils.LoadJSONIfExist(common.IconCachePath, &iconCache)
if err != nil {
logging.Error().Err(err).Msg("failed to load icon cache")
} else if len(iconCache) > 0 {
logging.Info().Int("count", len(iconCache)).Msg("icon cache loaded")
}
func init() {
go func() {
cleanupTicker := time.NewTicker(cleanUpInterval)
defer cleanupTicker.Stop()
@ -55,36 +46,21 @@ func InitIconCache() {
}
}
}()
task.OnProgramExit("save_favicon_cache", func() {
iconCacheMu.Lock()
defer iconCacheMu.Unlock()
if len(iconCache) == 0 {
return
}
if err := utils.SaveJSON(common.IconCachePath, &iconCache, 0o644); err != nil {
logging.Error().Err(err).Msg("failed to save icon cache")
}
})
}
func pruneExpiredIconCache() {
iconCacheMu.Lock()
defer iconCacheMu.Unlock()
nPruned := 0
for key, icon := range iconCache {
for key, icon := range iconCache.Range {
if icon.IsExpired() {
delete(iconCache, key)
iconCache.Delete(key)
nPruned++
}
}
if len(iconCache) > maxCacheEntries {
if iconCache.Size() > maxCacheEntries {
iconCache.Clear()
newIconCache := make(map[string]*cacheEntry, maxCacheEntries)
i := 0
for key, icon := range iconCache {
for key, icon := range iconCache.Range {
if i == maxCacheEntries {
break
}
@ -93,7 +69,9 @@ func pruneExpiredIconCache() {
i++
}
}
iconCache = newIconCache
for key, icon := range newIconCache {
iconCache.Store(key, icon)
}
}
if nPruned > 0 {
logging.Info().Int("pruned", nPruned).Msg("pruned expired icon cache")
@ -101,21 +79,18 @@ func pruneExpiredIconCache() {
}
func PruneRouteIconCache(route route) {
iconCacheMu.Lock()
defer iconCacheMu.Unlock()
delete(iconCache, route.Key())
iconCache.Delete(route.Key())
}
func loadIconCache(key string) *FetchResult {
iconCacheMu.RLock()
defer iconCacheMu.RUnlock()
icon, ok := iconCache[key]
iconMu.RLock()
defer iconMu.RUnlock()
icon, ok := iconCache.Load(key)
if ok && len(icon.Icon) > 0 {
logging.Debug().
Str("key", key).
Msg("icon found in cache")
icon.LastAccess.Store(time.Now())
icon.LastAccess.Store(utils.TimeNow())
return &FetchResult{Icon: icon.Icon, contentType: icon.ContentType}
}
return nil
@ -123,15 +98,17 @@ func loadIconCache(key string) *FetchResult {
func storeIconCache(key string, result *FetchResult) {
icon := result.Icon
if len(icon) > maxCacheSize {
if len(icon) > maxIconSize {
logging.Debug().Int("size", len(icon)).Msg("icon cache size exceeds max cache size")
return
}
iconCacheMu.Lock()
defer iconCacheMu.Unlock()
iconMu.Lock()
defer iconMu.Unlock()
entry := &cacheEntry{Icon: icon, ContentType: result.contentType}
entry.LastAccess.Store(time.Now())
iconCache[key] = entry
iconCache.Store(key, entry)
logging.Debug().Str("key", key).Int("size", len(icon)).Msg("stored icon cache")
}
@ -140,12 +117,20 @@ func (e *cacheEntry) IsExpired() bool {
}
func (e *cacheEntry) UnmarshalJSON(data []byte) error {
var tmp struct {
Icon []byte `json:"icon"`
ContentType string `json:"content_type,omitempty"`
LastAccess time.Time `json:"last_access"`
}
// check if data is json
if json.Valid(data) {
err := json.Unmarshal(data, &e)
err := json.Unmarshal(data, &tmp)
// return only if unmarshal is successful
// otherwise fallback to base64
if err == nil {
e.Icon = tmp.Icon
e.ContentType = tmp.ContentType
e.LastAccess.Store(tmp.LastAccess)
return nil
}
}

View file

@ -9,68 +9,78 @@ import (
type (
IconURL struct {
Value string `json:"value"`
FullValue string `json:"full_value"`
FullURL *string `json:"value,omitempty"` // only for absolute/relative icons
Extra *IconExtra `json:"extra,omitempty"` // only for walkxcode/selfhst icons
IconSource `json:"source"`
Extra *IconExtra `json:"extra"`
}
IconExtra struct {
Key IconKey `json:"key"`
Ref string `json:"ref"`
FileType string `json:"file_type"`
Name string `json:"name"`
IsLight bool `json:"is_light"`
IsDark bool `json:"is_dark"`
}
IconSource int
IconSource string
)
const (
IconSourceAbsolute IconSource = iota
IconSourceRelative
IconSourceWalkXCode
IconSourceSelfhSt
IconSourceAbsolute IconSource = "https://"
IconSourceRelative IconSource = "@target"
IconSourceWalkXCode IconSource = "@walkxcode"
IconSourceSelfhSt IconSource = "@selfhst"
)
var ErrInvalidIconURL = gperr.New("invalid icon url")
func NewSelfhStIconURL(reference, format string) *IconURL {
func NewIconURL(source IconSource, refOrName, format string) *IconURL {
switch source {
case IconSourceWalkXCode, IconSourceSelfhSt:
default:
panic("invalid icon source")
}
isLight, isDark := false, false
if strings.HasSuffix(refOrName, "-light") {
isLight = true
refOrName = strings.TrimSuffix(refOrName, "-light")
} else if strings.HasSuffix(refOrName, "-dark") {
isDark = true
refOrName = strings.TrimSuffix(refOrName, "-dark")
}
return &IconURL{
Value: reference + "." + format,
FullValue: fmt.Sprintf("@selfhst/%s.%s", reference, format),
IconSource: IconSourceSelfhSt,
IconSource: source,
Extra: &IconExtra{
Key: NewIconKey(source, refOrName),
FileType: format,
Name: reference,
Ref: refOrName,
IsLight: isLight,
IsDark: isDark,
},
}
}
func NewSelfhStIconURL(refOrName, format string) *IconURL {
return NewIconURL(IconSourceSelfhSt, refOrName, format)
}
func NewWalkXCodeIconURL(name, format string) *IconURL {
return &IconURL{
Value: name + "." + format,
FullValue: fmt.Sprintf("@walkxcode/%s.%s", name, format),
IconSource: IconSourceWalkXCode,
Extra: &IconExtra{
FileType: format,
Name: name,
},
}
return NewIconURL(IconSourceWalkXCode, name, format)
}
// HasIcon checks if the icon referenced by the IconURL exists in the cache based on its source.
// Returns false if the icon does not exist for IconSourceSelfhSt or IconSourceWalkXCode,
// otherwise returns true.
func (u *IconURL) HasIcon() bool {
if u.IconSource == IconSourceSelfhSt {
return HasSelfhstIcon(u.Extra.Name, u.Extra.FileType)
}
if u.IconSource == IconSourceWalkXCode {
return HasWalkxCodeIcon(u.Extra.Name, u.Extra.FileType)
}
return true
return HasIcon(u)
}
// Parse implements strutils.Parser.
func (u *IconURL) Parse(v string) error {
return u.parse(v, true)
}
func (u *IconURL) parse(v string, checkExists bool) error {
if v == "" {
return ErrInvalidIconURL
}
@ -78,33 +88,25 @@ func (u *IconURL) Parse(v string) error {
if slashIndex == -1 {
return ErrInvalidIconURL
}
u.FullValue = v
beforeSlash := v[:slashIndex]
switch beforeSlash {
case "http:", "https:":
u.Value = v
u.FullURL = &v
u.IconSource = IconSourceAbsolute
case "@target", "": // @target/favicon.ico, /favicon.ico
u.Value = v[slashIndex:]
u.IconSource = IconSourceRelative
if u.Value == "/" {
url := v[slashIndex:]
if url == "/" {
return ErrInvalidIconURL.Withf("%s", "empty path")
}
case "png", "svg", "webp": // walkxcode Icons
u.Value = v
u.IconSource = IconSourceWalkXCode
u.Extra = &IconExtra{
FileType: beforeSlash,
Name: strings.TrimSuffix(v[slashIndex+1:], "."+beforeSlash),
}
u.FullURL = &url
u.IconSource = IconSourceRelative
case "@selfhst", "@walkxcode": // selfh.st / walkxcode Icons, @selfhst/<reference>.<format>
u.Value = v[slashIndex+1:]
if beforeSlash == "@selfhst" {
u.IconSource = IconSourceSelfhSt
} else {
u.IconSource = IconSourceWalkXCode
}
parts := strings.Split(u.Value, ".")
parts := strings.Split(v[slashIndex+1:], ".")
if len(parts) != 2 {
return ErrInvalidIconURL.Withf("expect @%s/<reference>.<format>, e.g. @%s/adguard-home.webp", beforeSlash, beforeSlash)
}
@ -117,40 +119,67 @@ func (u *IconURL) Parse(v string) error {
default:
return ErrInvalidIconURL.Withf("%s", "invalid image format, expect svg/png/webp")
}
isLight, isDark := false, false
if strings.HasSuffix(reference, "-light") {
isLight = true
reference = strings.TrimSuffix(reference, "-light")
} else if strings.HasSuffix(reference, "-dark") {
isDark = true
reference = strings.TrimSuffix(reference, "-dark")
}
u.Extra = &IconExtra{
Key: NewIconKey(u.IconSource, reference),
FileType: format,
Name: reference,
Ref: reference,
IsLight: isLight,
IsDark: isDark,
}
if checkExists && !u.HasIcon() {
return ErrInvalidIconURL.Withf("no such icon %s.%s from %s", reference, format, u.IconSource)
}
default:
return ErrInvalidIconURL.Withf("%s", v)
return ErrInvalidIconURL.Subject(v)
}
if u.Value == "" {
return ErrInvalidIconURL.Withf("%s", "empty")
}
if !u.HasIcon() {
return ErrInvalidIconURL.Withf("no such icon %s from %s", u.Value, beforeSlash)
}
return nil
}
func (u *IconURL) URL() string {
if u.FullURL != nil {
return *u.FullURL
}
if u.Extra == nil {
return ""
}
filename := u.Extra.Ref
if u.Extra.IsLight {
filename += "-light"
} else if u.Extra.IsDark {
filename += "-dark"
}
switch u.IconSource {
case IconSourceAbsolute:
return u.Value
case IconSourceRelative:
return "/" + u.Value
case IconSourceWalkXCode:
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/%s/%s.%s", u.Extra.FileType, u.Extra.Name, u.Extra.FileType)
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/%s/%s.%s", u.Extra.FileType, filename, u.Extra.FileType)
case IconSourceSelfhSt:
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/selfhst/icons/%s/%s.%s", u.Extra.FileType, u.Extra.Name, u.Extra.FileType)
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/selfhst/icons/%s/%s.%s", u.Extra.FileType, filename, u.Extra.FileType)
}
return ""
}
func (u *IconURL) String() string {
return u.FullValue
if u.FullURL != nil {
return *u.FullURL
}
if u.Extra == nil {
return ""
}
var suffix string
if u.Extra.IsLight {
suffix = "-light"
} else if u.Extra.IsDark {
suffix = "-dark"
}
return fmt.Sprintf("%s/%s%s.%s", u.IconSource, u.Extra.Ref, suffix, u.Extra.FileType)
}
func (u *IconURL) MarshalText() ([]byte, error) {
@ -159,5 +188,5 @@ func (u *IconURL) MarshalText() ([]byte, error) {
// UnmarshalText implements encoding.TextUnmarshaler.
func (u *IconURL) UnmarshalText(data []byte) error {
return u.Parse(string(data))
return u.parse(string(data), false)
}

View file

@ -1,11 +1,16 @@
package homepage
package homepage_test
import (
"testing"
. "github.com/yusing/go-proxy/internal/homepage"
expect "github.com/yusing/go-proxy/internal/utils/testing"
)
func strPtr(s string) *string {
return &s
}
func TestIconURL(t *testing.T) {
tests := []struct {
name string
@ -17,7 +22,7 @@ func TestIconURL(t *testing.T) {
name: "absolute",
input: "http://example.com/icon.png",
wantValue: &IconURL{
Value: "http://example.com/icon.png",
FullURL: strPtr("http://example.com/icon.png"),
IconSource: IconSourceAbsolute,
},
},
@ -25,7 +30,7 @@ func TestIconURL(t *testing.T) {
name: "relative",
input: "@target/icon.png",
wantValue: &IconURL{
Value: "/icon.png",
FullURL: strPtr("/icon.png"),
IconSource: IconSourceRelative,
},
},
@ -33,7 +38,7 @@ func TestIconURL(t *testing.T) {
name: "relative2",
input: "/icon.png",
wantValue: &IconURL{
Value: "/icon.png",
FullURL: strPtr("/icon.png"),
IconSource: IconSourceRelative,
},
},
@ -49,25 +54,26 @@ func TestIconURL(t *testing.T) {
},
{
name: "walkxcode",
input: "png/adguard-home.png",
input: "@walkxcode/adguard-home.png",
wantValue: &IconURL{
Value: "png/adguard-home.png",
IconSource: IconSourceWalkXCode,
Extra: &IconExtra{
Key: NewIconKey(IconSourceWalkXCode, "adguard-home"),
FileType: "png",
Name: "adguard-home",
Ref: "adguard-home",
},
},
},
{
name: "walkxcode_alt",
input: "@walkxcode/adguard-home.png",
name: "walkxcode_light",
input: "@walkxcode/pfsense-light.png",
wantValue: &IconURL{
Value: "adguard-home.png",
IconSource: IconSourceWalkXCode,
Extra: &IconExtra{
Key: NewIconKey(IconSourceWalkXCode, "pfsense"),
FileType: "png",
Name: "adguard-home",
Ref: "pfsense",
IsLight: true,
},
},
},
@ -78,13 +84,39 @@ func TestIconURL(t *testing.T) {
},
{
name: "selfh.st_valid",
input: "@selfhst/adguard-home.png",
input: "@selfhst/adguard-home.webp",
wantValue: &IconURL{
Value: "adguard-home.png",
IconSource: IconSourceSelfhSt,
Extra: &IconExtra{
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"),
FileType: "webp",
Ref: "adguard-home",
},
},
},
{
name: "selfh.st_light",
input: "@selfhst/adguard-home-light.png",
wantValue: &IconURL{
IconSource: IconSourceSelfhSt,
Extra: &IconExtra{
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"),
FileType: "png",
Name: "adguard-home",
Ref: "adguard-home",
IsLight: true,
},
},
},
{
name: "selfh.st_dark",
input: "@selfhst/adguard-home-dark.svg",
wantValue: &IconURL{
IconSource: IconSourceSelfhSt,
Extra: &IconExtra{
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"),
FileType: "svg",
Ref: "adguard-home",
IsDark: true,
},
},
},
@ -116,7 +148,6 @@ func TestIconURL(t *testing.T) {
if tc.wantErr {
expect.ErrorIs(t, ErrInvalidIconURL, err)
} else {
tc.wantValue.FullValue = tc.input
expect.NoError(t, err)
expect.Equal(t, u, tc.wantValue)
}

View file

@ -0,0 +1,68 @@
package qbittorrent
import (
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/homepage/widgets"
)
type Client struct {
URL string
Username string
Password string
}
func (c *Client) Initialize(ctx context.Context, url string, cfg map[string]any) error {
c.URL = url
c.Username = cfg["username"].(string)
c.Password = cfg["password"].(string)
_, err := c.Version(ctx)
if err != nil {
return err
}
return nil
}
func (c *Client) doRequest(ctx context.Context, method, endpoint string, query url.Values, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, c.URL+endpoint+query.Encode(), body)
if err != nil {
return nil, err
}
if c.Username != "" && c.Password != "" {
req.SetBasicAuth(c.Username, c.Password)
}
resp, err := widgets.HTTPClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, gperr.Errorf("%w: %d %s", widgets.ErrHTTPStatus, resp.StatusCode, resp.Status)
}
return resp, nil
}
func jsonRequest[T any](ctx context.Context, client *Client, endpoint string, query url.Values) (result T, err error) {
resp, err := client.doRequest(ctx, http.MethodGet, endpoint, query, nil)
if err != nil {
return result, err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return result, err
}
return result, nil
}

Some files were not shown because too many files have changed in this diff Show more