Compare commits

..

32 Commits

Author SHA1 Message Date
yusing
6bb36e2e83 feat(autocert): generate unique ACME key paths per CA directory URL
Previously, ACME keys were stored at a single default path regardless of
which CA directory URL was configured. This caused key conflicts when
using multiple different ACME CAs.

Now, the key path is derived from a SHA256 hash of the CA directory URL,
allowing each CA to have its own key file:
- Default CA (Let's Encrypt): certs/acme.key
- Custom CA: certs/acme_<url_hash_16chars>.key

This enables running certificates against multiple ACME providers without
key collision issues.
2026-01-31 16:48:18 +08:00
yusing
4b57ef1cad fix(autocert): correct ObtainCert error handling
- ObtainCertIfNotExistsAll longer fail on fs.ErrNotExists
- Separate public LoadCertAll (loads all providers) from private loadCert
- LoadCertAll now uses allProviders() for iteration
- Updated tests to use LoadCertAll
2026-01-31 16:17:07 +08:00
yusing
3850a4a6e7 Merge branch 'main' into dev 2026-01-30 00:34:33 +08:00
yusing
da3c624582 Merge branch 'main' into dev 2026-01-27 00:39:19 +08:00
yusing
157a83bef8 Merge branch 'main' into dev 2026-01-27 00:06:26 +08:00
yusing
d61bd5ce51 chore: update go to 1.25.6 and dependencies 2026-01-16 18:34:39 +08:00
yusing
bad3e9a989 chore(README): remove zeabur badge 2026-01-16 14:21:37 +08:00
yusing
9adfd73121 fix(health): correct docker fallback url 2026-01-16 14:09:07 +08:00
yusing
4a652aaf55 fix(swagger): explicit set type names for IconFetchResult and IconMetaSearch 2026-01-16 12:26:14 +08:00
yusing
16c986978d chore(idlewatcher): remove junk comment 2026-01-16 11:35:40 +08:00
yusing
107b7c5f64 Merge branch 'main' into dev 2026-01-16 10:10:21 +08:00
yusing
818d75c8b7 Merge branch 'main' into dev 2026-01-04 12:43:18 +08:00
yusing
f1bc5de3ea Merge branch 'main' into dev 2026-01-04 12:28:32 +08:00
yusing
425ff0b25c Merge branch 'main' into dev 2026-01-02 22:12:11 +08:00
yusing
1f6614e337 refactor(config): correct logic in InitFromFile 2026-01-02 21:57:31 +08:00
yusing
9ba102a33d chore: update goutils 2026-01-02 21:56:55 +08:00
yusing
31c616246b Merge branch 'main' into dev 2026-01-02 15:49:20 +08:00
yusing
390859bd1f Merge branch 'main' into dev 2026-01-02 15:43:04 +08:00
yusing
243662c13b Merge branch 'main' into dev 2026-01-01 18:25:56 +08:00
yusing
588e9f5b18 Merge branch 'main' into dev 2025-12-30 22:01:48 +08:00
yusing
a3bf88cc9c chore(goutils): update subproject commit reference to 51a75d68 2025-12-30 22:00:28 +08:00
yusing
9b1af57859 Merge branch 'main' into dev 2025-12-30 21:52:24 +08:00
yusing
bb7471cc9c fix(tests/metrics): correct syntax error 2025-12-30 21:52:22 +08:00
yusing
a403b2b629 Merge branch 'main' into dev 2025-12-23 12:30:26 +08:00
yusing
54b9e7f236 Merge branch 'main' into dev 2025-12-22 17:15:02 +08:00
yusing
45b89cd452 fix(oidc): add trailing slash to OIDCAuthBasePath to work with paths like /authorize 2025-12-22 17:13:42 +08:00
yusing
72fea96c7b Merge branch 'main' into dev 2025-12-22 12:10:31 +08:00
yusing
aef646be6f Merge branch 'main' into dev 2025-12-22 10:45:44 +08:00
yusing
135a4ff6c7 Merge branch 'main' into dev 2025-12-20 19:31:12 +08:00
yusing
5f418b62c7 chore: upgrade dependencies 2025-12-17 17:37:58 +08:00
yusing
bd92c46375 refactor(http): enhance health check error logic by treating all 5xx as unhealthy 2025-12-17 12:24:04 +08:00
yusing
21a23dd147 fix(idlewatcher): directly serve the request on ready instead of redirecting 2025-12-17 11:48:22 +08:00
342 changed files with 5427 additions and 11338 deletions

View File

@@ -56,10 +56,6 @@ GODOXY_HTTP3_ENABLED=true
# API listening address # API listening address
GODOXY_API_ADDR=127.0.0.1:8888 GODOXY_API_ADDR=127.0.0.1:8888
# Local API listening address (unauthenticated, optional)
# Useful for local development, debugging or automation
GODOXY_LOCAL_API_ADDR=
# Metrics # Metrics
GODOXY_METRICS_DISABLE_CPU=false GODOXY_METRICS_DISABLE_CPU=false
GODOXY_METRICS_DISABLE_MEMORY=false GODOXY_METRICS_DISABLE_MEMORY=false

View File

@@ -1,60 +0,0 @@
name: GoDoxy CLI Binary
on:
pull_request:
paths:
- "cmd/cli/**"
- "internal/api/v1/docs/swagger.json"
- "Makefile"
- ".github/workflows/cli-binary.yml"
push:
branches:
- main
paths:
- "cmd/cli/**"
- "internal/api/v1/docs/swagger.json"
- "Makefile"
- ".github/workflows/cli-binary.yml"
tags:
- v*
workflow_dispatch:
jobs:
build:
strategy:
matrix:
include:
- runner: ubuntu-latest
platform: linux/amd64
binary_name: godoxy-cli-linux-amd64
- runner: ubuntu-24.04-arm
platform: linux/arm64
binary_name: godoxy-cli-linux-arm64
name: Build ${{ matrix.platform }}
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
with:
submodules: "recursive"
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Verify dependencies
run: go mod verify
- name: Build CLI
run: |
make CLI_BIN_PATH=bin/${{ matrix.binary_name }} build-cli
- name: Check binary
run: |
file bin/${{ matrix.binary_name }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary_name }}
path: bin/${{ matrix.binary_name }}

19
.gitignore vendored
View File

@@ -14,33 +14,34 @@ data/
debug/ debug/
logs/ logs/
log/
.vscode/settings.json .vscode/settings.json
go.work.sum
!cmd/**/ !cmd/**/
!internal/**/ !internal/**/
todo.md todo.md
.*.swp .*.swp
.aider*
mtrace.json mtrace.json
.env .env
*.env *.env
.cursorrules
.cursor/ .cursor/
.windsurfrules
test.Dockerfile test.Dockerfile
node_modules/
tsconfig.tsbuildinfo
!agent.compose.yml !agent.compose.yml
!agent/pkg/** !agent/pkg/**
dev-data/ dev-data/
RELEASE_NOTES.md RELEASE_NOTES.md
CLAUDE.md CLAUDE.md
.kilocode/** .kilocode/**
!.trunk/configs
# minified files
**/*-min.*
# generated CLI commands
cmd/cli/generated_commands.go

View File

@@ -47,7 +47,6 @@ linters:
errcheck: errcheck:
exclude-functions: exclude-functions:
- fmt.Fprintln - fmt.Fprintln
- (*gin.Context).Error # gin context error handler
forbidigo: forbidigo:
forbid: forbid:
- pattern: ^print(ln)?$ - pattern: ^print(ln)?$
@@ -56,15 +55,21 @@ linters:
statements: 120 statements: 120
gocyclo: gocyclo:
min-complexity: 14 min-complexity: 14
godoclint:
ignore: internal/api/v1/.+
godox: godox:
keywords: keywords:
- FIXME - FIXME
gomoddirectives:
replace-allow-list:
- github.com/abbot/go-http-auth
- github.com/gorilla/mux
- github.com/mailgun/minheap
- github.com/mailgun/multibuf
- github.com/jaguilar/vt100
- github.com/cucumber/godog
- github.com/http-wasm/http-wasm-host-go
govet: govet:
disable: disable:
- shadow - shadow
- fieldalignment
enable-all: true enable-all: true
misspell: misspell:
locale: US locale: US
@@ -101,7 +106,8 @@ linters:
checks: checks:
- all - all
- -SA1019 - -SA1019
- -QF1008 # keep embedded field selector for clarity dot-import-whitelist:
- github.com/yusing/godoxy/internal/utils/testing
tagalign: tagalign:
align: false align: false
sort: true sort: true
@@ -129,8 +135,9 @@ linters:
- legacy - legacy
- std-error-handling - std-error-handling
paths: paths:
- third_party$
- builtin$
- examples$ - examples$
- internal/api/v1/.+
formatters: formatters:
enable: enable:
- gofmt - gofmt
@@ -139,7 +146,6 @@ formatters:
exclusions: exclusions:
generated: lax generated: lax
paths: paths:
- third_party$
- builtin$
- examples$ - examples$
- internal/api/v1/.+
run:
tests: false

View File

@@ -1,2 +0,0 @@
# Prettier friendly markdownlint config (all formatting rules disabled)
extends: markdownlint/style/prettier

View File

@@ -1,7 +0,0 @@
rules:
quoted-strings:
required: only-when-needed
extra-allowed: ["{|}"]
key-duplicates: {}
octal-values:
forbid-implicit-octal: true

View File

@@ -7,45 +7,36 @@ cli:
plugins: plugins:
sources: sources:
- id: trunk - id: trunk
ref: v1.7.4 ref: v1.7.2
uri: https://github.com/trunk-io/plugins uri: https://github.com/trunk-io/plugins
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes) # Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
runtimes: runtimes:
enabled: enabled:
- node@22.16.0 - node@22.16.0
- python@3.10.8 - python@3.10.8
- go@1.26.0 - go@1.24.3
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration) # This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
lint: lint:
disabled: disabled:
- bandit - markdownlint
- black - yamllint
- isort
- ruff
enabled: enabled:
- yamllint@1.38.0 - checkov@3.2.471
- markdownlint@0.47.0 - golangci-lint2@2.5.0
- checkov@3.2.501
- golangci-lint2@2.9.0
- hadolint@2.14.0 - hadolint@2.14.0
- actionlint@1.7.10 - actionlint@1.7.7
- git-diff-check - git-diff-check
- gofmt@1.20.4 - gofmt@1.20.4
- osv-scanner@2.3.3 - osv-scanner@2.2.2
- oxipng@10.1.0 - oxipng@9.1.5
- prettier@3.8.1 - prettier@3.6.2
- shellcheck@0.11.0 - shellcheck@0.11.0
- shfmt@3.6.0 - shfmt@3.6.0
- trufflehog@3.93.3 - trufflehog@3.90.8
ignore:
- linters: [ALL]
paths:
- internal/api/v1/docs/**
actions: actions:
disabled: disabled:
- trunk-announce - trunk-announce
enabled:
- trunk-upgrade-available
- trunk-check-pre-push - trunk-check-pre-push
- trunk-fmt-pre-commit - trunk-fmt-pre-commit
enabled:
- trunk-upgrade-available

View File

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

View File

@@ -1,11 +1,10 @@
# Stage 1: deps # Stage 1: deps
FROM golang:1.26.0-alpine AS deps FROM golang:1.25.6-alpine AS deps
HEALTHCHECK NONE HEALTHCHECK NONE
# package version does not matter # package version does not matter
# libgcc and libstdc++ are needed for bun
# trunk-ignore(hadolint/DL3018) # trunk-ignore(hadolint/DL3018)
RUN apk add --no-cache tzdata make libcap-setcap libgcc libstdc++ RUN apk add --no-cache tzdata make libcap-setcap
ENV GOPATH=/root/go ENV GOPATH=/root/go
ENV GOCACHE=/root/.cache/go-build ENV GOCACHE=/root/.cache/go-build
@@ -18,10 +17,6 @@ COPY internal/gopsutil/go.mod internal/gopsutil/go.sum ./internal/gopsutil/
COPY internal/go-proxmox/go.mod internal/go-proxmox/go.sum ./internal/go-proxmox/ COPY internal/go-proxmox/go.mod internal/go-proxmox/go.sum ./internal/go-proxmox/
COPY go.mod go.sum ./ COPY go.mod go.sum ./
# for minify-js
COPY --from=oven/bun:1.3.9-alpine /usr/local/bin/bun /usr/local/bin/bun
COPY --from=oven/bun:1.3.9-alpine /usr/local/bin/bunx /usr/local/bin/bunx
# remove godoxy stuff from go.mod first # remove godoxy stuff from go.mod first
RUN --mount=type=cache,target=/root/.cache/go-build \ RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/go/pkg/mod \ --mount=type=cache,target=/root/go/pkg/mod \

View File

@@ -1,5 +1,5 @@
shell := /bin/sh shell := /bin/sh
export VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null) export VERSION ?= $(shell git describe --tags --abbrev=0)
export BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) export BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M') export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
export GOOS = linux export GOOS = linux
@@ -7,7 +7,7 @@ export GOOS = linux
REPO_URL ?= https://github.com/yusing/godoxy REPO_URL ?= https://github.com/yusing/godoxy
WEBUI_DIR ?= ../godoxy-webui WEBUI_DIR ?= ../godoxy-webui
DOCS_DIR ?= wiki DOCS_DIR ?= ${WEBUI_DIR}/wiki
ifneq ($(BRANCH), compat) ifneq ($(BRANCH), compat)
GO_TAGS = sonic GO_TAGS = sonic
@@ -58,7 +58,6 @@ endif
BUILD_FLAGS += -tags '$(GO_TAGS)' -ldflags='$(LDFLAGS)' BUILD_FLAGS += -tags '$(GO_TAGS)' -ldflags='$(LDFLAGS)'
BIN_PATH := $(shell pwd)/bin/${NAME} BIN_PATH := $(shell pwd)/bin/${NAME}
CLI_BIN_PATH ?= $(shell pwd)/bin/godoxy-cli
export NAME export NAME
export CGO_ENABLED export CGO_ENABLED
@@ -93,7 +92,7 @@ docker-build-test:
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2) 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) files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
gomod_paths := $(shell find . -name go.mod -type f | grep -vE '^./internal/(go-oidc|go-proxmox|gopsutil)/' | xargs dirname) gomod_paths := $(shell find . -name go.mod -type f | xargs dirname)
update-go: update-go:
for file in ${files}; do \ for file in ${files}; do \
@@ -118,27 +117,12 @@ mod-tidy:
cd ${PWD}/$$path && go mod tidy; \ cd ${PWD}/$$path && go mod tidy; \
done done
minify-js: build:
@if [ "${agent}" = "1" ]; then \
echo "minify-js: skipped for agent"; \
elif [ "${socket-proxy}" = "1" ]; then \
echo "minify-js: skipped for socket-proxy"; \
else \
for file in $$(find internal/ -name '*.js' | grep -v -- '-min\.js$$'); do \
ext="$${file##*.}"; \
base="$${file%.*}"; \
min_file="$${base}-min.$$ext"; \
echo "minifying $$file -> $$min_file"; \
bunx --bun uglify-js $$file --compress --mangle --output $$min_file; \
done \
fi
build: minify-js
mkdir -p $(shell dirname ${BIN_PATH}) mkdir -p $(shell dirname ${BIN_PATH})
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
${POST_BUILD} ${POST_BUILD}
run: minify-js run:
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
dev: dev:
@@ -148,12 +132,16 @@ dev-build: build
docker compose -f dev.compose.yml up -t 0 -d app --force-recreate docker compose -f dev.compose.yml up -t 0 -d app --force-recreate
benchmark: benchmark:
@TARGETS="$(TARGET)"; \ @if [ -z "$(TARGET)" ]; then \
if [ -z "$$TARGETS" ]; then TARGETS="godoxy traefik caddy nginx"; fi; \ docker compose -f dev.compose.yml up -d --force-recreate godoxy traefik caddy nginx; \
trap 'docker compose -f dev.compose.yml down $$TARGETS' EXIT; \ else \
docker compose -f dev.compose.yml up -d --force-recreate $$TARGETS; \ docker compose -f dev.compose.yml up -d --force-recreate $(TARGET); \
sleep 1; \ fi
./scripts/benchmark.sh sleep 1
@./scripts/benchmark.sh
dev-run: build
cd dev-data && ${BIN_PATH}
rapid-crash: rapid-crash:
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\ docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
@@ -179,20 +167,17 @@ gen-swagger:
python3 scripts/fix-swagger-json.py python3 scripts/fix-swagger-json.py
# we don't need this # we don't need this
rm internal/api/v1/docs/docs.go rm internal/api/v1/docs/docs.go
cp internal/api/v1/docs/swagger.json ${DOCS_DIR}/public/api.json
gen-swagger-markdown: gen-swagger
# brew tap go-swagger/go-swagger && brew install go-swagger
swagger generate markdown -f internal/api/v1/docs/swagger.yaml --skip-validation --output ${DOCS_DIR}/src/API.md
gen-api-types: gen-swagger gen-api-types: gen-swagger
# --disable-throw-on-error # --disable-throw-on-error
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \ bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
--responses -o ${WEBUI_DIR}/src/lib -n api.ts -p internal/api/v1/docs/swagger.json --responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
gen-cli: .PHONY: update-wiki
cd cmd/cli && go run ./gen
build-cli: gen-cli
mkdir -p $(shell dirname ${CLI_BIN_PATH})
go build -C cmd/cli -o ${CLI_BIN_PATH} .
.PHONY: gen-cli build-cli update-wiki
update-wiki: update-wiki:
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts

View File

@@ -36,6 +36,7 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
- [Proxmox Integration](#proxmox-integration) - [Proxmox Integration](#proxmox-integration)
- [Automatic Route Binding](#automatic-route-binding) - [Automatic Route Binding](#automatic-route-binding)
- [WebUI Management](#webui-management) - [WebUI Management](#webui-management)
- [API Endpoints](#api-endpoints)
- [Update / Uninstall system agent](#update--uninstall-system-agent) - [Update / Uninstall system agent](#update--uninstall-system-agent)
- [Screenshots](#screenshots) - [Screenshots](#screenshots)
- [idlesleeper](#idlesleeper) - [idlesleeper](#idlesleeper)
@@ -166,8 +167,23 @@ routes:
From the WebUI, you can: From the WebUI, you can:
- **LXC Lifecycle Control**: Start, stop, restart containers - **LXC Lifecycle Control**: Start, stop, restart containers
- **Node Logs**: Stream real-time journalctl or log files output from nodes - **Node Logs**: Stream real-time journalctl output from nodes
- **LXC Logs**: Stream real-time journalctl or log files output from containers - **LXC Logs**: Stream real-time journalctl output from containers
### API Endpoints
```http
# Node journalctl (WebSocket)
GET /api/v1/proxmox/journalctl/:node
# LXC journalctl (WebSocket)
GET /api/v1/proxmox/journalctl/:node/:vmid
# LXC lifecycle control
POST /api/v1/proxmox/lxc/:node/:vmid/start
POST /api/v1/proxmox/lxc/:node/:vmid/stop
POST /api/v1/proxmox/lxc/:node/:vmid/restart
```
## Update / Uninstall system agent ## Update / Uninstall system agent

View File

@@ -37,6 +37,7 @@
- [Proxmox 整合](#proxmox-整合) - [Proxmox 整合](#proxmox-整合)
- [自動路由綁定](#自動路由綁定) - [自動路由綁定](#自動路由綁定)
- [WebUI 管理](#webui-管理) - [WebUI 管理](#webui-管理)
- [API 端點](#api-端點)
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent) - [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
- [截圖](#截圖) - [截圖](#截圖)
- [閒置休眠](#閒置休眠) - [閒置休眠](#閒置休眠)
@@ -182,8 +183,23 @@ routes:
您可以從 WebUI 您可以從 WebUI
- **LXC 生命週期控制**:啟動、停止、重新啟動容器 - **LXC 生命週期控制**:啟動、停止、重新啟動容器
- **節點日誌**:串流節點的即時 journalctl 或日誌檔案輸出 - **節點日誌**:串流來自節點的即時 journalctl 輸出
- **LXC 日誌**:串流容器的即時 journalctl 或日誌檔案輸出 - **LXC 日誌**:串流來自容器的即時 journalctl 輸出
### API 端點
```http
# 節點 journalctl (WebSocket)
GET /api/v1/proxmox/journalctl/:node
# LXC journalctl (WebSocket)
GET /api/v1/proxmox/journalctl/:node/:vmid
# LXC 生命週期控制
POST /api/v1/proxmox/lxc/:node/:vmid/start
POST /api/v1/proxmox/lxc/:node/:vmid/stop
POST /api/v1/proxmox/lxc/:node/:vmid/restart
```
## 更新 / 卸載系統代理 (System Agent) ## 更新 / 卸載系統代理 (System Agent)

View File

@@ -19,6 +19,7 @@ import (
"github.com/yusing/godoxy/agent/pkg/handler" "github.com/yusing/godoxy/agent/pkg/handler"
"github.com/yusing/godoxy/internal/metrics/systeminfo" "github.com/yusing/godoxy/internal/metrics/systeminfo"
socketproxy "github.com/yusing/godoxy/socketproxy/pkg" socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings" strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task" "github.com/yusing/goutils/task"
"github.com/yusing/goutils/version" "github.com/yusing/goutils/version"
@@ -71,7 +72,7 @@ Tips:
// - Otherwise: route to HTTPS API handler // - Otherwise: route to HTTPS API handler
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: env.AgentPort}) tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: env.AgentPort})
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("failed to listen on port") gperr.LogFatal("failed to listen on port", err)
} }
caCertPool := x509.NewCertPool() caCertPool := x509.NewCertPool()
@@ -147,7 +148,7 @@ Tips:
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr) log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
l, err := net.Listen("tcp", socketproxy.ListenAddr) l, err := net.Listen("tcp", socketproxy.ListenAddr)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("failed to listen on port") gperr.LogFatal("failed to listen on port", err)
} }
errLog := log.Logger.With().Str("level", "error").Str("component", "socketproxy").Logger() errLog := log.Logger.With().Str("level", "error").Str("component", "socketproxy").Logger()
srv := http.Server{ srv := http.Server{
@@ -157,15 +158,10 @@ Tips:
}, },
ErrorLog: stdlog.New(&errLog, "", 0), ErrorLog: stdlog.New(&errLog, "", 0),
} }
go func() { srv.Serve(l)
err := srv.Serve(l)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Error().Err(err).Msg("socket proxy server stopped with error")
}
}()
} }
systeminfo.Poller.Start(t) systeminfo.Poller.Start()
task.WaitExit(3) task.WaitExit(3)
} }

View File

@@ -1,6 +1,6 @@
module github.com/yusing/godoxy/agent module github.com/yusing/godoxy/agent
go 1.26.0 go 1.25.6
exclude ( exclude (
github.com/moby/moby/api v1.53.0 // allow older daemon versions github.com/moby/moby/api v1.53.0 // allow older daemon versions
@@ -23,11 +23,11 @@ require (
github.com/bytedance/sonic v1.15.0 github.com/bytedance/sonic v1.15.0
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/pion/dtls/v3 v3.1.2 github.com/pion/dtls/v3 v3.0.10
github.com/pion/transport/v3 v3.1.1 github.com/pion/transport/v3 v3.1.1
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/yusing/godoxy v0.26.0 github.com/yusing/godoxy v0.25.2
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000 github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
github.com/yusing/goutils v0.7.0 github.com/yusing/goutils v0.7.0
) )
@@ -43,12 +43,12 @@ require (
github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.2.1+incompatible // indirect github.com/docker/cli v29.2.0+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect github.com/ebitengine/purego v0.9.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
@@ -60,10 +60,10 @@ require (
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
@@ -81,7 +81,7 @@ require (
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect
github.com/shirou/gopsutil/v4 v4.26.1 // indirect github.com/shirou/gopsutil/v4 v4.25.12 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect github.com/sirupsen/logrus v1.9.4 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect github.com/tklauser/numcpus v0.11.0 // indirect
@@ -90,20 +90,20 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.69.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect
github.com/yusing/ds v0.4.1 // indirect github.com/yusing/ds v0.4.1 // indirect
github.com/yusing/gointernals v0.2.0 // indirect github.com/yusing/gointernals v0.1.16 // indirect
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260218062549-0b0fa3a059ec // indirect github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468 // indirect
github.com/yusing/goutils/http/websocket v0.0.0-20260218062549-0b0fa3a059ec // indirect github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/arch v0.24.0 // indirect golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.50.0 // indirect golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.33.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@@ -37,8 +37,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg= github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -49,14 +49,14 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 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/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc= github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI= github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -93,14 +93,14 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 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 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54= github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ= github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU= github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0= github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -111,10 +111,10 @@ 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/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/luthermonson/go-proxmox v0.4.0 h1:LKXpG9d64zTaQF79wV0kfOnnSwIcdG39m7sc4ga+XZs= github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=
github.com/luthermonson/go-proxmox v0.4.0/go.mod h1:U6dAkJ+iiwaeb1g/LMWpWuWN4nmvWeXhmoMuYJMumS4= github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -145,16 +145,16 @@ github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.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 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
@@ -174,10 +174,10 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc= github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8= github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY= github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k= github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
@@ -210,38 +210,38 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig= github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk= github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtSVvg= github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
github.com/yusing/gointernals v0.2.0/go.mod h1:xGzNbPGMm5Z8kG0t4JYISMscw+gMQlgghkLxlgRZv5Y= github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 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/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -250,14 +250,14 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -1,4 +1,4 @@
# agent/pkg/agent # Agent Package
The `agent` package provides the client-side implementation for interacting with GoDoxy agents. It handles agent configuration, secure communication via TLS, and provides utilities for agent deployment and management. The `agent` package provides the client-side implementation for interacting with GoDoxy agents. It handles agent configuration, secure communication via TLS, and provides utilities for agent deployment and management.

View File

@@ -216,7 +216,7 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
cfg.l = log.With().Str("agent", cfg.Name).Logger() cfg.l = log.With().Str("agent", cfg.Name).Logger()
if err := streamUnsupportedErrs.Error(); err != nil { if err := streamUnsupportedErrs.Error(); err != nil {
cfg.l.Warn().Err(err).Msg("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work") gperr.LogWarn("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work", err, &cfg.l)
} }
if serverVersion.IsNewerThanMajor(cfg.Version) { if serverVersion.IsNewerThanMajor(cfg.Version) {

View File

@@ -1,4 +1,4 @@
# agent/pkg/agent/stream # Stream proxy protocol
This package implements a small header-based handshake that allows an authenticated client to request forwarding to a `(host, port)` destination. It supports both TCP-over-TLS and UDP-over-DTLS transports. This package implements a small header-based handshake that allows an authenticated client to request forwarding to a `(host, port)` destination. It supports both TCP-over-TLS and UDP-over-DTLS transports.

View File

@@ -21,10 +21,8 @@ const (
var version = [versionSize]byte{'0', '.', '1', '.', '0', 0, 0, 0} var version = [versionSize]byte{'0', '.', '1', '.', '0', 0, 0, 0}
var ( var ErrInvalidHeader = errors.New("invalid header")
ErrInvalidHeader = errors.New("invalid header") var ErrCloseImmediately = errors.New("close immediately")
ErrCloseImmediately = errors.New("close immediately")
)
type FlagType uint8 type FlagType uint8

View File

@@ -45,10 +45,9 @@ func TestTLSALPNMux_HTTPAndStreamShareOnePort(t *testing.T) {
defer func() { _ = tlsLn.Close() }() defer func() { _ = tlsLn.Close() }()
// HTTP server // HTTP server
httpSrv := &http.Server{ httpSrv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok"))
_, _ = w.Write([]byte("ok")) }),
}),
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){ TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) { stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
streamSrv.ServeConn(conn) streamSrv.ServeConn(conn)

View File

@@ -102,6 +102,7 @@ func TestUDPServer_RejectInvalidClient(t *testing.T) {
srv := startUDPServer(t, certs) srv := startUDPServer(t, certs)
// Try to connect with a client cert from a different CA // Try to connect with a client cert from a different CA
_, err = stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, invalidClientCert) _, err = stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, invalidClientCert)
require.Error(t, err, "expected error when connecting with client cert from different CA") require.Error(t, err, "expected error when connecting with client cert from different CA")

View File

@@ -1,4 +1,4 @@
FROM golang:1.26.0-alpine AS builder FROM golang:1.25.6-alpine AS builder
HEALTHCHECK NONE HEALTHCHECK NONE

View File

@@ -1,3 +1,3 @@
module github.com/yusing/godoxy/cmd/bench_server module github.com/yusing/godoxy/cmd/bench_server
go 1.26.0 go 1.25.6

View File

@@ -2,14 +2,13 @@ package main
import ( import (
"log" "log"
"math/rand/v2"
"net/http" "net/http"
"math/rand/v2"
) )
var ( var printables = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
printables = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" var random = make([]byte, 4096)
random = make([]byte, 4096)
)
func init() { func init() {
for i := range random { for i := range random {

View File

@@ -1,730 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/yusing/goutils/env"
)
type config struct {
Addr string
}
type stringSliceFlag struct {
set bool
v []string
}
func (s *stringSliceFlag) String() string {
return strings.Join(s.v, ",")
}
func (s *stringSliceFlag) Set(value string) error {
s.set = true
if value == "" {
s.v = nil
return nil
}
s.v = strings.Split(value, ",")
return nil
}
func run(args []string) error {
cfg, rest, err := parseGlobal(args)
if err != nil {
return err
}
if len(rest) == 0 {
printHelp()
return nil
}
if rest[0] == "help" {
printHelp()
return nil
}
ep, matchedLen := findEndpoint(rest)
if ep == nil {
ep, matchedLen = findEndpointAlias(rest)
}
if ep == nil {
return unknownCommandError(rest)
}
cmdArgs := rest[matchedLen:]
return executeEndpoint(cfg.Addr, *ep, cmdArgs)
}
func parseGlobal(args []string) (config, []string, error) {
var cfg config
fs := flag.NewFlagSet("godoxy", flag.ContinueOnError)
fs.SetOutput(io.Discard)
fs.StringVar(&cfg.Addr, "addr", "", "API address, e.g. 127.0.0.1:8888 or http://127.0.0.1:8888")
if err := fs.Parse(args); err != nil {
return cfg, nil, err
}
return cfg, fs.Args(), nil
}
func resolveBaseURL(addrFlag string) (string, error) {
if addrFlag != "" {
return normalizeURL(addrFlag), nil
}
_, _, _, fullURL := env.GetAddrEnv("LOCAL_API_ADDR", "", "http")
if fullURL == "" {
return "", errors.New("missing LOCAL_API_ADDR (or GODOXY_LOCAL_API_ADDR). set env var or pass --addr")
}
return normalizeURL(fullURL), nil
}
func normalizeURL(addr string) string {
a := strings.TrimSpace(addr)
if strings.Contains(a, "://") {
return strings.TrimRight(a, "/")
}
return "http://" + strings.TrimRight(a, "/")
}
func findEndpoint(args []string) (*Endpoint, int) {
var best *Endpoint
bestLen := -1
for i := range generatedEndpoints {
ep := &generatedEndpoints[i]
if len(ep.CommandPath) > len(args) {
continue
}
ok := true
for j, tok := range ep.CommandPath {
if args[j] != tok {
ok = false
break
}
}
if ok && len(ep.CommandPath) > bestLen {
best = ep
bestLen = len(ep.CommandPath)
}
}
return best, bestLen
}
func executeEndpoint(addrFlag string, ep Endpoint, args []string) error {
fs := flag.NewFlagSet(strings.Join(ep.CommandPath, "-"), flag.ContinueOnError)
fs.SetOutput(io.Discard)
useWS := false
if ep.IsWebSocket {
fs.BoolVar(&useWS, "ws", false, "use websocket")
}
typedValues := make(map[string]any, len(ep.Params))
isSet := make(map[string]bool, len(ep.Params))
for _, p := range ep.Params {
switch p.Type {
case "integer":
v := new(int)
fs.IntVar(v, p.FlagName, 0, p.Description)
typedValues[p.FlagName] = v
case "number":
v := new(float64)
fs.Float64Var(v, p.FlagName, 0, p.Description)
typedValues[p.FlagName] = v
case "boolean":
v := new(bool)
fs.BoolVar(v, p.FlagName, false, p.Description)
typedValues[p.FlagName] = v
case "array":
v := &stringSliceFlag{}
fs.Var(v, p.FlagName, p.Description+" (comma-separated)")
typedValues[p.FlagName] = v
default:
v := new(string)
fs.StringVar(v, p.FlagName, "", p.Description)
typedValues[p.FlagName] = v
}
}
if err := fs.Parse(args); err != nil {
return fmt.Errorf("%w\n\n%s", err, formatEndpointHelp(ep))
}
if len(fs.Args()) > 0 {
return fmt.Errorf("unexpected args: %s\n\n%s", strings.Join(fs.Args(), " "), formatEndpointHelp(ep))
}
fs.Visit(func(f *flag.Flag) {
isSet[f.Name] = true
})
for _, p := range ep.Params {
if !p.Required {
continue
}
if !isSet[p.FlagName] {
return fmt.Errorf("missing required flag --%s\n\n%s", p.FlagName, formatEndpointHelp(ep))
}
}
baseURL, err := resolveBaseURL(addrFlag)
if err != nil {
return err
}
reqURL, body, err := buildRequest(ep, baseURL, typedValues, isSet)
if err != nil {
return err
}
if useWS {
if !ep.IsWebSocket {
return errors.New("--ws is only supported for websocket endpoints")
}
return execWebsocket(ep, reqURL)
}
return execHTTP(ep, reqURL, body)
}
func buildRequest(ep Endpoint, baseURL string, typedValues map[string]any, isSet map[string]bool) (string, []byte, error) {
path := ep.Path
for _, p := range ep.Params {
if p.In != "path" {
continue
}
raw, err := paramValueString(p, typedValues[p.FlagName], isSet[p.FlagName])
if err != nil {
return "", nil, err
}
if raw == "" {
continue
}
esc := url.PathEscape(raw)
path = strings.ReplaceAll(path, "{"+p.Name+"}", esc)
path = strings.ReplaceAll(path, ":"+p.Name, esc)
}
u, err := url.Parse(baseURL)
if err != nil {
return "", nil, fmt.Errorf("invalid base url: %w", err)
}
u.Path = strings.TrimRight(u.Path, "/") + path
q := u.Query()
for _, p := range ep.Params {
if p.In != "query" || !isSet[p.FlagName] {
continue
}
val, err := paramQueryValues(p, typedValues[p.FlagName])
if err != nil {
return "", nil, err
}
for _, v := range val {
q.Add(p.Name, v)
}
}
u.RawQuery = q.Encode()
bodyMap := map[string]any{}
rawBody := ""
for _, p := range ep.Params {
if p.In != "body" || !isSet[p.FlagName] {
continue
}
if p.Name == "file" {
s, err := paramValueString(p, typedValues[p.FlagName], true)
if err != nil {
return "", nil, err
}
rawBody = s
continue
}
v, err := paramBodyValue(p, typedValues[p.FlagName])
if err != nil {
return "", nil, err
}
bodyMap[p.Name] = v
}
if rawBody != "" {
return u.String(), []byte(rawBody), nil
}
if len(bodyMap) == 0 {
return u.String(), nil, nil
}
data, err := json.Marshal(bodyMap)
if err != nil {
return "", nil, fmt.Errorf("marshal body: %w", err)
}
return u.String(), data, nil
}
func paramValueString(p Param, raw any, wasSet bool) (string, error) {
if !wasSet {
return "", nil
}
switch v := raw.(type) {
case *string:
return *v, nil
case *int:
return strconv.Itoa(*v), nil
case *float64:
return strconv.FormatFloat(*v, 'f', -1, 64), nil
case *bool:
if *v {
return "true", nil
}
return "false", nil
case *stringSliceFlag:
return strings.Join(v.v, ","), nil
default:
return "", fmt.Errorf("unsupported flag value for %s", p.FlagName)
}
}
func paramQueryValues(p Param, raw any) ([]string, error) {
switch v := raw.(type) {
case *string:
return []string{*v}, nil
case *int:
return []string{strconv.Itoa(*v)}, nil
case *float64:
return []string{strconv.FormatFloat(*v, 'f', -1, 64)}, nil
case *bool:
if *v {
return []string{"true"}, nil
}
return []string{"false"}, nil
case *stringSliceFlag:
if len(v.v) == 0 {
return nil, nil
}
return v.v, nil
default:
return nil, fmt.Errorf("unsupported query flag type for %s", p.FlagName)
}
}
func paramBodyValue(p Param, raw any) (any, error) {
switch v := raw.(type) {
case *string:
if p.Type == "object" || p.Type == "array" {
var decoded any
if err := json.Unmarshal([]byte(*v), &decoded); err != nil {
return nil, fmt.Errorf("invalid JSON for --%s: %w", p.FlagName, err)
}
return decoded, nil
}
return *v, nil
case *int:
return *v, nil
case *float64:
return *v, nil
case *bool:
return *v, nil
case *stringSliceFlag:
return v.v, nil
default:
return nil, fmt.Errorf("unsupported body flag type for %s", p.FlagName)
}
}
func execHTTP(ep Endpoint, reqURL string, body []byte) error {
var r io.Reader
if body != nil {
r = bytes.NewReader(body)
}
req, err := http.NewRequest(ep.Method, reqURL, r)
if err != nil {
return err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
payload, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
if len(payload) == 0 {
return fmt.Errorf("%s %s failed: %s", ep.Method, ep.Path, resp.Status)
}
return fmt.Errorf("%s %s failed: %s: %s", ep.Method, ep.Path, resp.Status, strings.TrimSpace(string(payload)))
}
printJSON(payload)
return nil
}
func execWebsocket(ep Endpoint, reqURL string) error {
wsURL := strings.Replace(reqURL, "http://", "ws://", 1)
wsURL = strings.Replace(wsURL, "https://", "wss://", 1)
if strings.ToUpper(ep.Method) != http.MethodGet {
return fmt.Errorf("--ws requires GET endpoint, got %s", ep.Method)
}
c, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
return err
}
defer c.Close()
stopPing := make(chan struct{})
defer close(stopPing)
go func() {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for {
select {
case <-stopPing:
return
case <-ticker.C:
_ = c.SetWriteDeadline(time.Now().Add(2 * time.Second))
if err := c.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil {
return
}
}
}
}()
for {
_, msg, err := c.ReadMessage()
if err != nil {
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) || strings.Contains(err.Error(), "close") {
return nil
}
return err
}
if string(msg) == "pong" {
continue
}
fmt.Println(string(msg))
}
}
func printJSON(payload []byte) {
if len(payload) == 0 {
fmt.Println("null")
return
}
var v any
if err := json.Unmarshal(payload, &v); err != nil {
fmt.Println(strings.TrimSpace(string(payload)))
return
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
_ = enc.Encode(v)
}
func printHelp() {
fmt.Println("godoxy [--addr ADDR] <command>")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" godoxy version")
fmt.Println(" godoxy route list")
fmt.Println(" godoxy route route --which whoami")
fmt.Println()
printGroupedCommands()
}
func printGroupedCommands() {
grouped := map[string][]Endpoint{}
groupOrder := make([]string, 0)
seen := map[string]bool{}
for _, ep := range generatedEndpoints {
group := "root"
if len(ep.CommandPath) > 1 {
group = ep.CommandPath[0]
}
grouped[group] = append(grouped[group], ep)
if !seen[group] {
seen[group] = true
groupOrder = append(groupOrder, group)
}
}
sort.Strings(groupOrder)
for _, group := range groupOrder {
fmt.Printf("Commands (%s):\n", group)
sort.Slice(grouped[group], func(i, j int) bool {
li := strings.Join(grouped[group][i].CommandPath, " ")
lj := strings.Join(grouped[group][j].CommandPath, " ")
return li < lj
})
maxCmdWidth := 0
for _, ep := range grouped[group] {
cmd := strings.Join(ep.CommandPath, " ")
if len(cmd) > maxCmdWidth {
maxCmdWidth = len(cmd)
}
}
for _, ep := range grouped[group] {
cmd := strings.Join(ep.CommandPath, " ")
fmt.Printf(" %-*s %s\n", maxCmdWidth, cmd, ep.Summary)
}
fmt.Println()
}
}
func unknownCommandError(rest []string) error {
cmd := strings.Join(rest, " ")
var b strings.Builder
b.WriteString("unknown command: ")
b.WriteString(cmd)
if len(rest) > 0 && hasGroup(rest[0]) {
if len(rest) > 1 {
if hint := nearestForGroup(rest[0], rest[1]); hint != "" {
b.WriteString("\nDo you mean ")
b.WriteString(hint)
b.WriteString("?")
}
}
b.WriteString("\n\n")
b.WriteString(formatGroupHelp(rest[0]))
return errors.New(b.String())
}
if hint := nearestCommand(cmd); hint != "" {
b.WriteString("\nDo you mean ")
b.WriteString(hint)
b.WriteString("?")
}
b.WriteString("\n\n")
b.WriteString("Run `godoxy help` for available commands.")
return errors.New(b.String())
}
func findEndpointAlias(args []string) (*Endpoint, int) {
var best *Endpoint
bestLen := -1
for i := range generatedEndpoints {
alias := aliasCommandPath(generatedEndpoints[i])
if len(alias) == 0 || len(alias) > len(args) {
continue
}
ok := true
for j, tok := range alias {
if args[j] != tok {
ok = false
break
}
}
if ok && len(alias) > bestLen {
best = &generatedEndpoints[i]
bestLen = len(alias)
}
}
return best, bestLen
}
func aliasCommandPath(ep Endpoint) []string {
rawPath := strings.TrimPrefix(ep.Path, "/api/v1/")
rawPath = strings.Trim(rawPath, "/")
if rawPath == "" {
return nil
}
parts := strings.Split(rawPath, "/")
if len(parts) == 1 {
if isPathParam(parts[0]) {
return nil
}
return []string{toKebabToken(parts[0])}
}
if isPathParam(parts[0]) || isPathParam(parts[1]) {
return nil
}
return []string{toKebabToken(parts[0]), toKebabToken(parts[1])}
}
func isPathParam(s string) bool {
return strings.HasPrefix(s, "{") || strings.HasPrefix(s, ":")
}
func toKebabToken(s string) string {
s = strings.ReplaceAll(s, "_", "-")
return strings.ToLower(strings.Trim(s, "-"))
}
func hasGroup(group string) bool {
for _, ep := range generatedEndpoints {
if len(ep.CommandPath) > 1 && ep.CommandPath[0] == group {
return true
}
}
return false
}
func nearestCommand(input string) string {
commands := make([]string, 0, len(generatedEndpoints))
for _, ep := range generatedEndpoints {
commands = append(commands, strings.Join(ep.CommandPath, " "))
}
return nearestByDistance(input, commands)
}
func nearestForGroup(group, input string) string {
choiceSet := map[string]struct{}{}
for _, ep := range generatedEndpoints {
if len(ep.CommandPath) < 2 || ep.CommandPath[0] != group {
continue
}
choiceSet[ep.CommandPath[1]] = struct{}{}
alias := aliasCommandPath(ep)
if len(alias) == 2 && alias[0] == group {
choiceSet[alias[1]] = struct{}{}
}
}
choices := make([]string, 0, len(choiceSet))
for choice := range choiceSet {
choices = append(choices, choice)
}
if len(choices) == 0 {
return ""
}
return group + " " + nearestByDistance(input, choices)
}
func formatGroupHelp(group string) string {
commands := make([]Endpoint, 0)
for _, ep := range generatedEndpoints {
if len(ep.CommandPath) > 1 && ep.CommandPath[0] == group {
commands = append(commands, ep)
}
}
sort.Slice(commands, func(i, j int) bool {
return strings.Join(commands[i].CommandPath, " ") < strings.Join(commands[j].CommandPath, " ")
})
maxWidth := 0
for _, ep := range commands {
cmd := strings.Join(ep.CommandPath, " ")
if len(cmd) > maxWidth {
maxWidth = len(cmd)
}
}
var b strings.Builder
fmt.Fprintf(&b, "Available subcommands for %s:\n", group)
for _, ep := range commands {
cmd := strings.Join(ep.CommandPath, " ")
fmt.Fprintf(&b, " %-*s %s\n", maxWidth, cmd, ep.Summary)
}
return strings.TrimRight(b.String(), "\n")
}
func formatEndpointHelp(ep Endpoint) string {
cmd := "godoxy " + strings.Join(ep.CommandPath, " ")
var b strings.Builder
fmt.Fprintf(&b, "Usage: %s [flags]\n", cmd)
if ep.Summary != "" {
fmt.Fprintf(&b, "Summary: %s\n", ep.Summary)
}
if alias := aliasCommandPath(ep); len(alias) > 0 && strings.Join(alias, " ") != strings.Join(ep.CommandPath, " ") {
fmt.Fprintf(&b, "Alias: godoxy %s\n", strings.Join(alias, " "))
}
params := make([]Param, 0, len(ep.Params))
params = append(params, ep.Params...)
if ep.IsWebSocket {
params = append(params, Param{
FlagName: "ws",
Type: "boolean",
Description: "use websocket",
Required: false,
})
}
if len(params) == 0 {
return strings.TrimRight(b.String(), "\n")
}
b.WriteString("Flags:\n")
maxWidth := 0
flagNames := make([]string, 0, len(params))
for _, p := range params {
name := "--" + p.FlagName
if p.Required {
name += " (required)"
}
flagNames = append(flagNames, name)
if len(name) > maxWidth {
maxWidth = len(name)
}
}
for i, p := range params {
desc := p.Description
if desc == "" {
desc = p.In + " " + p.Type
}
fmt.Fprintf(&b, " %-*s %s\n", maxWidth, flagNames[i], desc)
}
return strings.TrimRight(b.String(), "\n")
}
func nearestByDistance(input string, choices []string) string {
if len(choices) == 0 {
return ""
}
nearest := choices[0]
minDistance := levenshteinDistance(input, nearest)
for _, choice := range choices[1:] {
d := levenshteinDistance(input, choice)
if d < minDistance {
minDistance = d
nearest = choice
}
}
return nearest
}
//nolint:intrange
func levenshteinDistance(a, b string) int {
if a == b {
return 0
}
if len(a) == 0 {
return len(b)
}
if len(b) == 0 {
return len(a)
}
v0 := make([]int, len(b)+1)
v1 := make([]int, len(b)+1)
for i := 0; i <= len(b); i++ {
v0[i] = i
}
for i := 0; i < len(a); i++ {
v1[0] = i + 1
for j := 0; j < len(b); j++ {
cost := 0
if a[i] != b[j] {
cost = 1
}
v1[j+1] = min3(v1[j]+1, v0[j+1]+1, v0[j]+cost)
}
for j := 0; j <= len(b); j++ {
v0[j] = v1[j]
}
}
return v1[len(b)]
}
func min3(a, b, c int) int {
if a < b && a < c {
return a
}
if b < a && b < c {
return b
}
return c
}

View File

@@ -1,366 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"go/format"
"os"
"path/filepath"
"sort"
"strings"
"unicode"
)
type swaggerSpec struct {
BasePath string `json:"basePath"`
Paths map[string]map[string]operation `json:"paths"`
Definitions map[string]definition `json:"definitions"`
}
type operation struct {
OperationID string `json:"operationId"`
Summary string `json:"summary"`
Tags []string `json:"tags"`
Parameters []parameter `json:"parameters"`
}
type parameter struct {
Name string `json:"name"`
In string `json:"in"`
Required bool `json:"required"`
Type string `json:"type"`
Description string `json:"description"`
Schema *schemaRef `json:"schema"`
}
type schemaRef struct {
Ref string `json:"$ref"`
}
type definition struct {
Type string `json:"type"`
Required []string `json:"required"`
Properties map[string]definition `json:"properties"`
Items *definition `json:"items"`
}
type endpoint struct {
CommandPath []string
Method string
Path string
Summary string
IsWebSocket bool
Params []param
}
type param struct {
FlagName string
Name string
In string
Type string
Required bool
Description string
}
func main() {
root := filepath.Join("..", "..")
inPath := filepath.Join(root, "internal", "api", "v1", "docs", "swagger.json")
outPath := "generated_commands.go"
raw, err := os.ReadFile(inPath)
must(err)
var spec swaggerSpec
must(json.Unmarshal(raw, &spec))
eps := buildEndpoints(spec)
must(writeGenerated(outPath, eps))
}
func buildEndpoints(spec swaggerSpec) []endpoint {
byCommand := map[string]endpoint{}
pathKeys := make([]string, 0, len(spec.Paths))
for p := range spec.Paths {
pathKeys = append(pathKeys, p)
}
sort.Strings(pathKeys)
for _, p := range pathKeys {
methodMap := spec.Paths[p]
methods := make([]string, 0, len(methodMap))
for m := range methodMap {
methods = append(methods, strings.ToUpper(m))
}
sort.Strings(methods)
for _, method := range methods {
op := methodMap[strings.ToLower(method)]
if op.OperationID == "" {
continue
}
ep := endpoint{
CommandPath: commandPathFromOp(p, op.OperationID),
Method: method,
Path: ensureSlash(spec.BasePath) + normalizePath(p),
Summary: op.Summary,
IsWebSocket: hasTag(op.Tags, "websocket"),
Params: collectParams(spec, op),
}
key := strings.Join(ep.CommandPath, " ")
if existing, ok := byCommand[key]; ok {
if betterEndpoint(ep, existing) {
byCommand[key] = ep
}
continue
}
byCommand[key] = ep
}
}
out := make([]endpoint, 0, len(byCommand))
for _, ep := range byCommand {
out = append(out, ep)
}
sort.Slice(out, func(i, j int) bool {
ai := strings.Join(out[i].CommandPath, " ")
aj := strings.Join(out[j].CommandPath, " ")
return ai < aj
})
return out
}
func commandPathFromOp(path, opID string) []string {
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) == 0 {
return []string{toKebab(opID)}
}
if len(parts) == 1 {
return []string{toKebab(parts[0])}
}
group := toKebab(parts[0])
name := toKebab(opID)
if name == group {
name = "get"
}
if group == "v1" {
return []string{name}
}
return []string{group, name}
}
func collectParams(spec swaggerSpec, op operation) []param {
params := make([]param, 0)
for _, p := range op.Parameters {
switch p.In {
case "body":
if p.Schema != nil && p.Schema.Ref != "" {
defName := strings.TrimPrefix(p.Schema.Ref, "#/definitions/")
params = append(params, bodyParamsFromDef(spec.Definitions[defName])...)
continue
}
params = append(params, param{
FlagName: toKebab(p.Name),
Name: p.Name,
In: "body",
Type: defaultType(p.Type),
Required: p.Required,
Description: p.Description,
})
default:
params = append(params, param{
FlagName: toKebab(p.Name),
Name: p.Name,
In: p.In,
Type: defaultType(p.Type),
Required: p.Required,
Description: p.Description,
})
}
}
// Deduplicate by flag name, prefer required entries.
byFlag := map[string]param{}
for _, p := range params {
if cur, ok := byFlag[p.FlagName]; ok {
if !cur.Required && p.Required {
byFlag[p.FlagName] = p
}
continue
}
byFlag[p.FlagName] = p
}
out := make([]param, 0, len(byFlag))
for _, p := range byFlag {
out = append(out, p)
}
sort.Slice(out, func(i, j int) bool {
if out[i].In != out[j].In {
return out[i].In < out[j].In
}
return out[i].FlagName < out[j].FlagName
})
return out
}
func bodyParamsFromDef(def definition) []param {
if def.Type != "object" {
return nil
}
requiredSet := map[string]struct{}{}
for _, name := range def.Required {
requiredSet[name] = struct{}{}
}
keys := make([]string, 0, len(def.Properties))
for k := range def.Properties {
keys = append(keys, k)
}
sort.Strings(keys)
out := make([]param, 0, len(keys))
for _, k := range keys {
prop := def.Properties[k]
_, required := requiredSet[k]
t := defaultType(prop.Type)
if prop.Type == "array" {
t = "array"
}
if prop.Type == "object" {
t = "object"
}
out = append(out, param{
FlagName: toKebab(k),
Name: k,
In: "body",
Type: t,
Required: required,
})
}
return out
}
func betterEndpoint(a, b endpoint) bool {
// Prefer GET, then fewer path params, then shorter path.
if a.Method == "GET" && b.Method != "GET" {
return true
}
if a.Method != "GET" && b.Method == "GET" {
return false
}
ac := countPathParams(a.Path)
bc := countPathParams(b.Path)
if ac != bc {
return ac < bc
}
return len(a.Path) < len(b.Path)
}
func countPathParams(path string) int {
count := 0
for _, seg := range strings.Split(path, "/") {
if strings.HasPrefix(seg, "{") || strings.HasPrefix(seg, ":") {
count++
}
}
return count
}
func normalizePath(p string) string {
parts := strings.Split(p, "/")
for i, part := range parts {
if strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") {
name := strings.TrimSuffix(strings.TrimPrefix(part, "{"), "}")
parts[i] = "{" + name + "}"
}
}
return strings.Join(parts, "/")
}
func hasTag(tags []string, want string) bool {
for _, t := range tags {
if strings.EqualFold(t, want) {
return true
}
}
return false
}
func writeGenerated(outPath string, eps []endpoint) error {
var b bytes.Buffer
b.WriteString("// Code generated by cmd/cli/gen. DO NOT EDIT.\n")
b.WriteString("package main\n\n")
b.WriteString("var generatedEndpoints = []Endpoint{\n")
for _, ep := range eps {
b.WriteString("\t{\n")
fmt.Fprintf(&b, "\t\tCommandPath: %#v,\n", ep.CommandPath)
fmt.Fprintf(&b, "\t\tMethod: %q,\n", ep.Method)
fmt.Fprintf(&b, "\t\tPath: %q,\n", ep.Path)
fmt.Fprintf(&b, "\t\tSummary: %q,\n", ep.Summary)
fmt.Fprintf(&b, "\t\tIsWebSocket: %t,\n", ep.IsWebSocket)
b.WriteString("\t\tParams: []Param{\n")
for _, p := range ep.Params {
b.WriteString("\t\t\t{\n")
fmt.Fprintf(&b, "\t\t\t\tFlagName: %q,\n", p.FlagName)
fmt.Fprintf(&b, "\t\t\t\tName: %q,\n", p.Name)
fmt.Fprintf(&b, "\t\t\t\tIn: %q,\n", p.In)
fmt.Fprintf(&b, "\t\t\t\tType: %q,\n", p.Type)
fmt.Fprintf(&b, "\t\t\t\tRequired: %t,\n", p.Required)
fmt.Fprintf(&b, "\t\t\t\tDescription: %q,\n", p.Description)
b.WriteString("\t\t\t},\n")
}
b.WriteString("\t\t},\n")
b.WriteString("\t},\n")
}
b.WriteString("}\n")
formatted, err := format.Source(b.Bytes())
if err != nil {
return fmt.Errorf("format generated source: %w", err)
}
return os.WriteFile(outPath, formatted, 0o644)
}
func ensureSlash(s string) string {
if strings.HasPrefix(s, "/") {
return s
}
return "/" + s
}
func defaultType(t string) string {
switch t {
case "integer", "number", "boolean", "array", "object", "string":
return t
default:
return "string"
}
}
func toKebab(s string) string {
if s == "" {
return s
}
s = strings.ReplaceAll(s, "_", "-")
s = strings.ReplaceAll(s, ".", "-")
var out []rune
for i, r := range s {
if unicode.IsUpper(r) {
if i > 0 && out[len(out)-1] != '-' {
out = append(out, '-')
}
out = append(out, unicode.ToLower(r))
continue
}
out = append(out, unicode.ToLower(r))
}
res := strings.Trim(string(out), "-")
for strings.Contains(res, "--") {
res = strings.ReplaceAll(res, "--", "-")
}
return res
}
func must(err error) {
if err != nil {
panic(err)
}
}

View File

@@ -1,10 +0,0 @@
module github.com/yusing/godoxy/cli
go 1.26.0
require (
github.com/gorilla/websocket v1.5.3
github.com/yusing/goutils v0.7.0
)
replace github.com/yusing/goutils => ../../goutils

View File

@@ -1,10 +0,0 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,13 +0,0 @@
package main
import (
"fmt"
"os"
)
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}

View File

@@ -1,19 +0,0 @@
package main
type Param struct {
FlagName string
Name string
In string
Type string
Required bool
Description string
}
type Endpoint struct {
CommandPath []string
Method string
Path string
Summary string
IsWebSocket bool
Params []Param
}

View File

@@ -7,7 +7,6 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/api" "github.com/yusing/godoxy/internal/api"
apiV1 "github.com/yusing/godoxy/internal/api/v1" apiV1 "github.com/yusing/godoxy/internal/api/v1"
agentApi "github.com/yusing/godoxy/internal/api/v1/agent" agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
@@ -129,31 +128,25 @@ func listenDebugServer() {
mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/svg+xml") w.Header().Set("Content-Type", "image/svg+xml")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`) w.Write([]byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`))
}) })
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler) mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler) mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
apiHandler := newAPIHandler(mux) apiHandler := newApiHandler(mux)
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP) mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
mux.Finalize() mux.Finalize()
go func() { go http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//nolint:gosec w.Header().Set("Pragma", "no-cache")
err := http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") mux.mux.ServeHTTP(w, r)
w.Header().Set("Expires", "0") }))
mux.mux.ServeHTTP(w, r)
}))
if err != nil {
log.Err(err).Msg("Error starting debug server")
}
}()
} }
func newAPIHandler(debugMux *debugMux) *gin.Engine { func newApiHandler(debugMux *debugMux) *gin.Engine {
r := gin.New() r := gin.New()
r.Use(api.ErrorHandler()) r.Use(api.ErrorHandler())
r.Use(api.ErrorLoggingMiddleware()) r.Use(api.ErrorLoggingMiddleware())
@@ -188,6 +181,7 @@ func newAPIHandler(debugMux *debugMux) *gin.Engine {
registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon) registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon)
registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health) registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health)
registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons) registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons)
registerGinRoute(v1, "POST", "Config reload", "/reload", apiV1.Reload)
registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats) registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats)
route := v1.Group("/route") route := v1.Group("/route")

View File

@@ -1,4 +1,4 @@
FROM golang:1.26.0-alpine AS builder FROM golang:1.25.6-alpine AS builder
HEALTHCHECK NONE HEALTHCHECK NONE

View File

@@ -1,7 +1,7 @@
module github.com/yusing/godoxy/cmd/h2c_test_server module github.com/yusing/godoxy/cmd/h2c_test_server
go 1.26.0 go 1.25.6
require golang.org/x/net v0.50.0 require golang.org/x/net v0.49.0
require golang.org/x/text v0.34.0 // indirect require golang.org/x/text v0.33.0 // indirect

View File

@@ -1,4 +1,4 @@
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=

View File

@@ -1,12 +1,12 @@
package main package main
import ( import (
"errors"
"os" "os"
"sync" "sync"
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/api"
"github.com/yusing/godoxy/internal/auth" "github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/common" "github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/config" "github.com/yusing/godoxy/internal/config"
@@ -14,8 +14,12 @@ import (
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list" iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
"github.com/yusing/godoxy/internal/logging" "github.com/yusing/godoxy/internal/logging"
"github.com/yusing/godoxy/internal/logging/memlogger" "github.com/yusing/godoxy/internal/logging/memlogger"
"github.com/yusing/godoxy/internal/metrics/systeminfo"
"github.com/yusing/godoxy/internal/metrics/uptime"
"github.com/yusing/godoxy/internal/net/gphttp/middleware" "github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/route/rules" "github.com/yusing/godoxy/internal/route/rules"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/task" "github.com/yusing/goutils/task"
"github.com/yusing/goutils/version" "github.com/yusing/goutils/version"
) )
@@ -47,6 +51,7 @@ func main() {
parallel( parallel(
dnsproviders.InitProviders, dnsproviders.InitProviders,
iconlist.InitCache, iconlist.InitCache,
systeminfo.Poller.Start,
middleware.LoadComposeFiles, middleware.LoadComposeFiles,
) )
@@ -61,19 +66,35 @@ func main() {
err := config.Load() err := config.Load()
if err != nil { if err != nil {
if criticalErr, ok := errors.AsType[config.CriticalError](err); ok { gperr.LogWarn("errors in config", err)
log.Fatal().Err(criticalErr).Msg("critical error in config")
}
log.Warn().Err(err).Msg("errors in config")
} }
config.StartProxyServers()
if err := auth.Initialize(); err != nil { if err := auth.Initialize(); err != nil {
log.Fatal().Err(err).Msg("failed to initialize authentication") log.Fatal().Err(err).Msg("failed to initialize authentication")
} }
rules.InitAuthHandler(auth.AuthOrProceed) rules.InitAuthHandler(auth.AuthOrProceed)
// API Handler needs to start after auth is initialized.
server.StartServer(task.RootTask("api_server", false), server.Options{
Name: "api",
HTTPAddr: common.APIHTTPAddr,
Handler: api.NewHandler(true),
})
// Local API Handler is used for unauthenticated access.
if common.LocalAPIHTTPAddr != "" {
server.StartServer(task.RootTask("local_api_server", false), server.Options{
Name: "local_api",
HTTPAddr: common.LocalAPIHTTPAddr,
Handler: api.NewHandler(false),
})
}
listenDebugServer() listenDebugServer()
uptime.Poller.Start()
config.WatchChanges() config.WatchChanges()
close(done) close(done)

View File

@@ -31,8 +31,8 @@ services:
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000} user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
read_only: true read_only: true
tmpfs: tmpfs:
- /tmp:rw - /app/.next/cache # next image caching
- /app/node_modules/.cache:rw
# for lite variant, do not change uid/gid # for lite variant, do not change uid/gid
# - /var/cache/nginx:uid=101,gid=101 # - /var/cache/nginx:uid=101,gid=101
# - /run:uid=101,gid=101 # - /run:uid=101,gid=101

86
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/yusing/godoxy module github.com/yusing/godoxy
go 1.26.0 go 1.25.6
exclude ( exclude (
github.com/moby/moby/api v1.53.0 // allow older daemon versions github.com/moby/moby/api v1.53.0 // allow older daemon versions
@@ -21,23 +21,22 @@ replace (
require ( require (
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
github.com/cenkalti/backoff/v5 v5.0.3 // backoff for retrying operations
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
github.com/fsnotify/fsnotify v1.9.0 // file watcher github.com/fsnotify/fsnotify v1.9.0 // file watcher
github.com/gin-gonic/gin v1.11.0 // api server github.com/gin-gonic/gin v1.11.0 // api server
github.com/go-acme/lego/v4 v4.32.0 // acme client github.com/go-acme/lego/v4 v4.31.0 // acme client
github.com/go-playground/validator/v10 v10.30.1 // validator github.com/go-playground/validator/v10 v10.30.1 // validator
github.com/gobwas/glob v0.2.3 // glob matcher for route rules github.com/gobwas/glob v0.2.3 // glob matcher for route rules
github.com/gorilla/websocket v1.5.3 // websocket for API and agent github.com/gorilla/websocket v1.5.3 // websocket for API and agent
github.com/gotify/server/v2 v2.9.0 // reference the Message struct for json response github.com/gotify/server/v2 v2.8.0 // reference the Message struct for json response
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
github.com/pires/go-proxyproto v0.11.0 // proxy protocol support github.com/pires/go-proxyproto v0.9.2 // proxy protocol support
github.com/puzpuzpuz/xsync/v4 v4.4.0 // lock free map for concurrent operations github.com/puzpuzpuz/xsync/v4 v4.4.0 // lock free map for concurrent operations
github.com/rs/zerolog v1.34.0 // logging github.com/rs/zerolog v1.34.0 // logging
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
golang.org/x/crypto v0.48.0 // encrypting password with bcrypt golang.org/x/crypto v0.47.0 // encrypting password with bcrypt
golang.org/x/net v0.50.0 // HTTP header utilities golang.org/x/net v0.49.0 // HTTP header utilities
golang.org/x/oauth2 v0.35.0 // oauth2 authentication golang.org/x/oauth2 v0.34.0 // oauth2 authentication
golang.org/x/sync v0.19.0 // errgroup and singleflight for concurrent operations golang.org/x/sync v0.19.0 // errgroup and singleflight for concurrent operations
golang.org/x/time v0.14.0 // time utilities golang.org/x/time v0.14.0 // time utilities
) )
@@ -45,30 +44,30 @@ require (
require ( require (
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
github.com/bytedance/sonic v1.15.0 // fast json parsing github.com/bytedance/sonic v1.15.0 // fast json parsing
github.com/docker/cli v29.2.1+incompatible // needs docker/cli/cli/connhelper connection helper for docker client github.com/docker/cli v29.2.0+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.3.1 // jwt authentication github.com/golang-jwt/jwt/v5 v5.3.1 // jwt authentication
github.com/luthermonson/go-proxmox v0.4.0 // proxmox API client github.com/luthermonson/go-proxmox v0.3.2 // proxmox API client
github.com/moby/moby/api v1.52.0 // docker API github.com/moby/moby/api v1.52.0 // docker API
github.com/moby/moby/client v0.2.1 // docker client github.com/moby/moby/client v0.2.1 // docker client
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
github.com/quic-go/quic-go v0.59.0 // http3 support github.com/quic-go/quic-go v0.59.0 // http3 support
github.com/shirou/gopsutil/v4 v4.26.1 // system information github.com/shirou/gopsutil/v4 v4.25.12 // system information
github.com/spf13/afero v1.15.0 // afero for file system operations github.com/spf13/afero v1.15.0 // afero for file system operations
github.com/stretchr/testify v1.11.1 // testing framework github.com/stretchr/testify v1.11.1 // testing framework
github.com/valyala/fasthttp v1.69.0 // fast http for health check github.com/valyala/fasthttp v1.69.0 // fast http for health check
github.com/yusing/ds v0.4.1 // data structures and algorithms github.com/yusing/ds v0.4.1 // data structures and algorithms
github.com/yusing/godoxy/agent v0.0.0-20260218101334-add7884a365e github.com/yusing/godoxy/agent v0.0.0-20260129101716-0f13004ad6ba
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260218101334-add7884a365e github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260129101716-0f13004ad6ba
github.com/yusing/gointernals v0.2.0 github.com/yusing/gointernals v0.1.16
github.com/yusing/goutils v0.7.0 github.com/yusing/goutils v0.7.0
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260218062549-0b0fa3a059ec github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468
github.com/yusing/goutils/http/websocket v0.0.0-20260218062549-0b0fa3a059ec github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468
github.com/yusing/goutils/server v0.0.0-20260218062549-0b0fa3a059ec github.com/yusing/goutils/server v0.0.0-20260129081554-24e52ede7468
) )
require ( require (
cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
@@ -90,7 +89,7 @@ require (
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect github.com/ebitengine/purego v0.9.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
@@ -99,8 +98,8 @@ require (
github.com/gofrs/flock v0.13.0 // indirect github.com/gofrs/flock v0.13.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/jinzhu/copier v0.4.0 // indirect github.com/jinzhu/copier v0.4.0 // indirect
@@ -125,26 +124,26 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/samber/lo v1.52.0 // indirect github.com/samber/lo v1.52.0 // indirect
github.com/samber/slog-common v0.20.0 // indirect github.com/samber/slog-common v0.19.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.1 // indirect github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect github.com/sirupsen/logrus v1.9.4 // indirect
github.com/sony/gobreaker v1.0.0 // indirect github.com/sony/gobreaker v1.0.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.uber.org/atomic v1.11.0 go.uber.org/atomic v1.11.0
go.uber.org/ratelimit v0.3.1 // indirect go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/mod v0.33.0 // indirect golang.org/x/mod v0.32.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.42.0 // indirect golang.org/x/tools v0.41.0 // indirect
google.golang.org/api v0.267.0 // indirect google.golang.org/api v0.263.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.79.1 // indirect google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.1 // indirect gopkg.in/ini.v1 v1.67.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
@@ -156,6 +155,7 @@ require (
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/boombuler/barcode v1.1.0 // indirect github.com/boombuler/barcode v1.1.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect
@@ -165,20 +165,20 @@ require (
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-resty/resty/v2 v2.17.2 // indirect github.com/go-resty/resty/v2 v2.17.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/google/go-querystring v1.2.0 // indirect github.com/google/go-querystring v1.2.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/linode/linodego v1.65.0 // indirect github.com/linode/linodego v1.64.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/nrdcg/goinwx v0.12.0 // indirect github.com/nrdcg/goinwx v0.12.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 // indirect github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 // indirect github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pion/dtls/v3 v3.1.2 // indirect github.com/pion/dtls/v3 v3.0.10 // indirect
github.com/pion/logging v0.2.4 // indirect github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect github.com/pion/transport/v4 v4.0.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
@@ -190,7 +190,7 @@ require (
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect github.com/ulikunitz/xz v0.5.15 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vultr/govultr/v3 v3.27.0 // indirect github.com/vultr/govultr/v3 v3.26.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/arch v0.24.0 // indirect golang.org/x/arch v0.23.0 // indirect
) )

152
go.sum
View File

@@ -1,5 +1,5 @@
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
@@ -76,8 +76,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg= github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -94,14 +94,14 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 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/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc= github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI= github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -122,8 +122,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 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/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
@@ -151,14 +151,14 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ= github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 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/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54= github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ= github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= 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 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
@@ -177,8 +177,8 @@ github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0= github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/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 h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
@@ -191,12 +191,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 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/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k= github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY=
github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8= github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -227,10 +227,10 @@ github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg= github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4= github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0= github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk= github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 h1:eMzyN+jGJbxG4ut278uwIsUo9XacXc711lFjhKnaUso=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8= github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU= github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 h1:t34IpOa+8NfmjkU8bdWtYrLrmr346/FGhu8FlpJDQok=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc= github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0/go.mod h1:p95/OxVsdx71I2Qrck1GtIS87sRxcTRKXzUi5nWm9NY=
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -245,14 +245,14 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -278,10 +278,10 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc= github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8= github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY= github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k= github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
@@ -320,8 +320,8 @@ github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZy
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= 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/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8= github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
@@ -329,49 +329,49 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig= github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk= github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtSVvg= github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
github.com/yusing/gointernals v0.2.0/go.mod h1:xGzNbPGMm5Z8kG0t4JYISMscw+gMQlgghkLxlgRZv5Y= github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 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/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -381,10 +381,10 @@ 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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -412,8 +412,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.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.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.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -432,8 +432,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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.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.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -442,21 +442,21 @@ 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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE= google.golang.org/api v0.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk=
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

Submodule goutils updated: 3be815cb6e...52ea531e95

View File

@@ -1,4 +1,4 @@
# internal/acl # ACL (Access Control List)
Access control at the TCP connection level with IP/CIDR, timezone, and country-based filtering. Access control at the TCP connection level with IP/CIDR, timezone, and country-based filtering.
@@ -54,13 +54,13 @@ type Matchers []Matcher
### Exported functions and methods ### Exported functions and methods
```go ```go
func (c *Config) Validate() error func (c *Config) Validate() gperr.Error
``` ```
Validates configuration and sets defaults. Must be called before `Start`. Validates configuration and sets defaults. Must be called before `Start`.
```go ```go
func (c *Config) Start(parent task.Parent) error func (c *Config) Start(parent task.Parent) gperr.Error
``` ```
Initializes the ACL, starts the logger and notification goroutines. Initializes the ACL, starts the logger and notification goroutines.
@@ -169,14 +169,14 @@ Configuration is loaded from `config/config.yml` under the `acl` key.
```yaml ```yaml
acl: acl:
default: "allow" # "allow" or "deny" default: "allow" # "allow" or "deny"
allow_local: true # Allow private/loopback IPs allow_local: true # Allow private/loopback IPs
log: log:
log_allowed: false # Log allowed connections log_allowed: false # Log allowed connections
notify: notify:
to: ["gotify"] # Notification providers to: ["gotify"] # Notification providers
interval: "1m" # Notification interval interval: "1m" # Notification interval
include_allowed: false # Include allowed in notifications include_allowed: false # Include allowed in notifications
``` ```
### Hot-reloading ### Hot-reloading

View File

@@ -14,7 +14,6 @@ import (
"github.com/yusing/godoxy/internal/maxmind" "github.com/yusing/godoxy/internal/maxmind"
"github.com/yusing/godoxy/internal/notif" "github.com/yusing/godoxy/internal/notif"
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
aclevents "github.com/yusing/goutils/events/acl"
strutils "github.com/yusing/goutils/strings" strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task" "github.com/yusing/goutils/task"
) )
@@ -67,16 +66,16 @@ type config struct {
type checkCache struct { type checkCache struct {
*maxmind.IPInfo *maxmind.IPInfo
allow bool allow bool
reason string
created time.Time created time.Time
} }
type ipLog struct { type ipLog struct {
info *maxmind.IPInfo info *maxmind.IPInfo
allowed bool allowed bool
reason string
} }
type ContextKey struct{}
const cacheTTL = 1 * time.Minute const cacheTTL = 1 * time.Minute
func (c *checkCache) Expired() bool { func (c *checkCache) Expired() bool {
@@ -90,7 +89,7 @@ const (
ACLDeny = "deny" ACLDeny = "deny"
) )
func (c *Config) Validate() error { func (c *Config) Validate() gperr.Error {
switch c.Default { switch c.Default {
case "", ACLAllow: case "", ACLAllow:
c.defaultAllow = true c.defaultAllow = true
@@ -134,10 +133,7 @@ func (c *Config) Valid() bool {
return c != nil && c.valErr == nil return c != nil && c.valErr == nil
} }
func (c *Config) Start(parent task.Parent) error { func (c *Config) Start(parent task.Parent) gperr.Error {
if c.valErr != nil {
return c.valErr
}
if c.Log != nil { if c.Log != nil {
logger, err := accesslog.NewAccessLogger(parent, c.Log) logger, err := accesslog.NewAccessLogger(parent, c.Log)
if err != nil { if err != nil {
@@ -145,6 +141,9 @@ func (c *Config) Start(parent task.Parent) error {
} }
c.logger = logger c.logger = logger
} }
if c.valErr != nil {
return c.valErr
}
if c.needLogOrNotify() { if c.needLogOrNotify() {
c.logNotifyCh = make(chan ipLog, 100) c.logNotifyCh = make(chan ipLog, 100)
@@ -171,14 +170,13 @@ func (c *Config) Start(parent task.Parent) error {
return nil return nil
} }
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool, reason string) { func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
if common.ForceResolveCountry && info.City == nil { if common.ForceResolveCountry && info.City == nil {
maxmind.LookupCity(info) maxmind.LookupCity(info)
} }
c.ipCache.Store(info.Str, &checkCache{ c.ipCache.Store(info.Str, &checkCache{
IPInfo: info, IPInfo: info,
allow: allow, allow: allow,
reason: reason,
created: time.Now(), created: time.Now(),
}) })
} }
@@ -215,26 +213,23 @@ func (c *Config) logNotifyLoop(parent task.Parent) {
select { select {
case <-parent.Context().Done(): case <-parent.Context().Done():
return return
case req := <-c.logNotifyCh: case log := <-c.logNotifyCh:
if c.logger != nil { if c.logger != nil {
if !req.allowed || c.logAllowed { if !log.allowed || c.logAllowed {
c.logger.LogACL(req.info, !req.allowed, req.reason) c.logger.LogACL(log.info, !log.allowed)
} }
} }
if c.needNotify() { if c.needNotify() {
if req.allowed { if log.allowed {
if c.notifyAllowed { if c.notifyAllowed {
c.allowedCount[req.info.Str]++ c.allowedCount[log.info.Str]++
c.totalAllowedCount++ c.totalAllowedCount++
} }
} else { } else {
c.blockedCount[req.info.Str]++ c.blockedCount[log.info.Str]++
c.totalBlockedCount++ c.totalBlockedCount++
} }
} }
if !req.allowed {
aclevents.Blocked(req.info.Str, req.reason)
}
case <-c.notifyTicker.C: // will never tick when notify is disabled case <-c.notifyTicker.C: // will never tick when notify is disabled
total := len(c.allowedCount) + len(c.blockedCount) total := len(c.allowedCount) + len(c.blockedCount)
if total == 0 { if total == 0 {
@@ -266,9 +261,9 @@ func (c *Config) logNotifyLoop(parent task.Parent) {
} }
// log and notify if needed // log and notify if needed
func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool, reason string) { func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool) {
if c.logNotifyCh != nil { if c.logNotifyCh != nil {
c.logNotifyCh <- ipLog{info: info, allowed: allowed, reason: reason} c.logNotifyCh <- ipLog{info: info, allowed: allowed}
} }
} }
@@ -283,36 +278,30 @@ func (c *Config) IPAllowed(ip net.IP) bool {
} }
if c.allowLocal && ip.IsPrivate() { if c.allowLocal && ip.IsPrivate() {
c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true, "allowed by allow_local rule") c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
return true return true
} }
ipStr := ip.String() ipStr := ip.String()
record, ok := c.ipCache.Load(ipStr) record, ok := c.ipCache.Load(ipStr)
if ok && !record.Expired() { if ok && !record.Expired() {
c.logAndNotify(record.IPInfo, record.allow, record.reason) c.logAndNotify(record.IPInfo, record.allow)
return record.allow return record.allow
} }
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr} ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
if index := c.Deny.MatchedIndex(ipAndStr); index != -1 { if c.Deny.Match(ipAndStr) {
reason := "blocked by deny rule: " + c.Deny[index].raw c.logAndNotify(ipAndStr, false)
c.logAndNotify(ipAndStr, false, reason) c.cacheRecord(ipAndStr, false)
c.cacheRecord(ipAndStr, false, reason)
return false return false
} }
if index := c.Allow.MatchedIndex(ipAndStr); index != -1 { if c.Allow.Match(ipAndStr) {
reason := "allowed by allow rule: " + c.Allow[index].raw c.logAndNotify(ipAndStr, true)
c.logAndNotify(ipAndStr, true, reason) c.cacheRecord(ipAndStr, true)
c.cacheRecord(ipAndStr, true, reason)
return true return true
} }
reason := "denied by default" c.logAndNotify(ipAndStr, c.defaultAllow)
if c.defaultAllow { c.cacheRecord(ipAndStr, c.defaultAllow)
reason = "allowed by default"
}
c.logAndNotify(ipAndStr, c.defaultAllow, reason)
c.cacheRecord(ipAndStr, c.defaultAllow, reason)
return c.defaultAllow return c.defaultAllow
} }

View File

@@ -2,7 +2,6 @@ package acl
import ( import (
"bytes" "bytes"
"errors"
"net" "net"
"strings" "strings"
@@ -39,9 +38,9 @@ var errMatcherFormat = gperr.Multiline().AddLines(
) )
var ( var (
errSyntax = errors.New("syntax error") errSyntax = gperr.New("syntax error")
errInvalidIP = errors.New("invalid IP") errInvalidIP = gperr.New("invalid IP")
errInvalidCIDR = errors.New("invalid CIDR") errInvalidCIDR = gperr.New("invalid CIDR")
) )
func (matcher *Matcher) Parse(s string) error { func (matcher *Matcher) Parse(s string) error {
@@ -83,15 +82,6 @@ func (matchers Matchers) Match(ip *maxmind.IPInfo) bool {
return false return false
} }
func (matchers Matchers) MatchedIndex(ip *maxmind.IPInfo) int {
for i, m := range matchers {
if m.match(ip) {
return i
}
}
return -1
}
func (matchers Matchers) MarshalText() ([]byte, error) { func (matchers Matchers) MarshalText() ([]byte, error) {
if len(matchers) == 0 { if len(matchers) == 0 {
return []byte("[]"), nil return []byte("[]"), nil

View File

@@ -5,8 +5,6 @@ import (
"io" "io"
"net" "net"
"time" "time"
"github.com/rs/zerolog/log"
) )
type TCPListener struct { type TCPListener struct {
@@ -46,7 +44,6 @@ func (s *TCPListener) Accept() (net.Conn, error) {
} }
addr, ok := c.RemoteAddr().(*net.TCPAddr) addr, ok := c.RemoteAddr().(*net.TCPAddr)
if !ok { if !ok {
log.Error().Msgf("unexpected remote address type: %T, addr: %s", c.RemoteAddr(), c.RemoteAddr().String())
// Not a TCPAddr, drop // Not a TCPAddr, drop
c.Close() c.Close()
return noConn{}, nil return noConn{}, nil

View File

@@ -1,9 +0,0 @@
package acl
import "net"
type ACL interface {
IPAllowed(ip net.IP) bool
WrapTCP(l net.Listener) net.Listener
WrapUDP(l net.PacketConn) net.PacketConn
}

View File

@@ -1,16 +0,0 @@
package acl
import "context"
type ContextKey struct{}
func SetCtx(ctx interface{ SetValue(key any, value any) }, acl ACL) {
ctx.SetValue(ContextKey{}, acl)
}
func FromCtx(ctx context.Context) ACL {
if acl, ok := ctx.Value(ContextKey{}).(ACL); ok {
return acl
}
return nil
}

View File

@@ -4,8 +4,6 @@ import (
"errors" "errors"
"net" "net"
"time" "time"
"github.com/rs/zerolog/log"
) )
type UDPListener struct { type UDPListener struct {
@@ -35,7 +33,6 @@ func (s *UDPListener) ReadFrom(p []byte) (int, net.Addr, error) {
} }
udpAddr, ok := addr.(*net.UDPAddr) udpAddr, ok := addr.(*net.UDPAddr)
if !ok { if !ok {
log.Error().Msgf("unexpected remote address type: %T, addr: %s", addr, addr.String())
// Not a UDPAddr, drop // Not a UDPAddr, drop
continue continue
} }
@@ -55,7 +52,6 @@ func (s *UDPListener) WriteTo(p []byte, addr net.Addr) (int, error) {
} }
udpAddr, ok := addr.(*net.UDPAddr) udpAddr, ok := addr.(*net.UDPAddr)
if !ok { if !ok {
log.Error().Msgf("unexpected remote address type: %T, addr: %s", addr, addr.String())
// Not a UDPAddr, drop // Not a UDPAddr, drop
continue continue
} }

View File

@@ -1,4 +1,4 @@
# internal/agentpool # Agent Pool
Thread-safe pool for managing remote Docker agent connections. Thread-safe pool for managing remote Docker agent connections.

View File

@@ -34,10 +34,7 @@ func newAgent(cfg *agent.AgentConfig) *Agent {
if addr != agent.AgentHost+":443" { if addr != agent.AgentHost+":443" {
return nil, &net.AddrError{Err: "invalid address", Addr: addr} return nil, &net.AddrError{Err: "invalid address", Addr: addr}
} }
dialer := &net.Dialer{ return net.DialTimeout("tcp", cfg.Addr, timeout)
Timeout: timeout,
}
return dialer.Dial("tcp", cfg.Addr)
}, },
TLSConfig: cfg.TLSConfig(), TLSConfig: cfg.TLSConfig(),
ReadTimeout: 5 * time.Second, ReadTimeout: 5 * time.Second,

View File

@@ -10,24 +10,24 @@ import (
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
agentPkg "github.com/yusing/godoxy/agent/pkg/agent" "github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/goutils/http/reverseproxy" "github.com/yusing/goutils/http/reverseproxy"
) )
func (agent *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) { func (cfg *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, agentPkg.APIBaseURL+endpoint, body) req, err := http.NewRequestWithContext(ctx, method, agent.APIBaseURL+endpoint, body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return agent.httpClient.Do(req) return cfg.httpClient.Do(req)
} }
func (agent *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) { func (cfg *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) {
req.URL.Host = agentPkg.AgentHost req.URL.Host = agent.AgentHost
req.URL.Scheme = "https" req.URL.Scheme = "https"
req.URL.Path = agentPkg.APIEndpointBase + endpoint req.URL.Path = agent.APIEndpointBase + endpoint
req.RequestURI = "" req.RequestURI = ""
resp, err := agent.httpClient.Do(req) resp, err := cfg.httpClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -40,20 +40,20 @@ type HealthCheckResponse struct {
Latency time.Duration `json:"latency"` Latency time.Duration `json:"latency"`
} }
func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) { func (cfg *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
req := fasthttp.AcquireRequest() req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req) defer fasthttp.ReleaseRequest(req)
resp := fasthttp.AcquireResponse() resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp) defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI(agentPkg.APIBaseURL + agentPkg.EndpointHealth + "?" + query) req.SetRequestURI(agent.APIBaseURL + agent.EndpointHealth + "?" + query)
req.Header.SetMethod(fasthttp.MethodGet) req.Header.SetMethod(fasthttp.MethodGet)
req.Header.Set("Accept-Encoding", "identity") req.Header.Set("Accept-Encoding", "identity")
req.SetConnectionClose() req.SetConnectionClose()
start := time.Now() start := time.Now()
err = agent.fasthttpHcClient.DoTimeout(req, resp, timeout) err = cfg.fasthttpHcClient.DoTimeout(req, resp, timeout)
ret.Latency = time.Since(start) ret.Latency = time.Since(start)
if err != nil { if err != nil {
return ret, err return ret, err
@@ -71,14 +71,14 @@ func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret Heal
return ret, nil return ret, nil
} }
func (agent *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) { func (cfg *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
transport := agent.Transport() transport := cfg.Transport()
dialer := websocket.Dialer{ dialer := websocket.Dialer{
NetDialContext: transport.DialContext, NetDialContext: transport.DialContext,
NetDialTLSContext: transport.DialTLSContext, NetDialTLSContext: transport.DialTLSContext,
} }
return dialer.DialContext(ctx, agentPkg.APIBaseURL+endpoint, http.Header{ return dialer.DialContext(ctx, agent.APIBaseURL+endpoint, http.Header{
"Host": {agentPkg.AgentHost}, "Host": {agent.AgentHost},
}) })
} }
@@ -86,9 +86,9 @@ func (agent *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.
// //
// It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint // It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint
// If the request has a query, it will be added to the proxy request's URL // If the request has a query, it will be added to the proxy request's URL
func (agent *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) { func (cfg *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
rp := reverseproxy.NewReverseProxy("agent", agentPkg.AgentURL, agent.Transport()) rp := reverseproxy.NewReverseProxy("agent", agent.AgentURL, cfg.Transport())
req.URL.Host = agentPkg.AgentHost req.URL.Host = agent.AgentHost
req.URL.Scheme = "https" req.URL.Scheme = "https"
req.URL.Path = endpoint req.URL.Path = endpoint
req.RequestURI = "" req.RequestURI = ""

View File

@@ -21,23 +21,22 @@ import (
"github.com/yusing/godoxy/internal/auth" "github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/common" "github.com/yusing/godoxy/internal/common"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
) )
// NewHandler creates a new Gin engine for the API.
//
// @title GoDoxy API // @title GoDoxy API
// @version 1.0 // @version 1.0
// @description GoDoxy API // @description GoDoxy API
// @termsOfService https://github.com/yusing/godoxy/blob/main/LICENSE // @termsOfService https://github.com/yusing/godoxy/blob/main/LICENSE
//
// @contact.name Yusing // @contact.name Yusing
// @contact.url https://github.com/yusing/godoxy/issues // @contact.url https://github.com/yusing/godoxy/issues
//
// @license.name MIT // @license.name MIT
// @license.url https://github.com/yusing/godoxy/blob/main/LICENSE // @license.url https://github.com/yusing/godoxy/blob/main/LICENSE
//
// @BasePath /api/v1 // @BasePath /api/v1
//
// @externalDocs.description GoDoxy Docs // @externalDocs.description GoDoxy Docs
// @externalDocs.url https://docs.godoxy.dev // @externalDocs.url https://docs.godoxy.dev
func NewHandler(requireAuth bool) *gin.Engine { func NewHandler(requireAuth bool) *gin.Engine {
@@ -77,8 +76,8 @@ func NewHandler(requireAuth bool) *gin.Engine {
v1.GET("/favicon", apiV1.FavIcon) v1.GET("/favicon", apiV1.FavIcon)
v1.GET("/health", apiV1.Health) v1.GET("/health", apiV1.Health)
v1.GET("/icons", apiV1.Icons) v1.GET("/icons", apiV1.Icons)
v1.POST("/reload", apiV1.Reload)
v1.GET("/stats", apiV1.Stats) v1.GET("/stats", apiV1.Stats)
v1.GET("/events", apiV1.Events)
route := v1.Group("/route") route := v1.Group("/route")
{ {
@@ -205,8 +204,9 @@ func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
c.Next() c.Next()
if len(c.Errors) > 0 { if len(c.Errors) > 0 {
logger := log.With().Str("uri", c.Request.RequestURI).Logger()
for _, err := range c.Errors { for _, err := range c.Errors {
log.Err(err.Err).Str("uri", c.Request.RequestURI).Msg("Internal error") gperr.LogError("Internal error", err.Err, &logger)
} }
if !c.IsWebsocket() { if !c.IsWebsocket() {
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error")) c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))

View File

@@ -1,4 +1,4 @@
# internal/api/v1 # API v1 Package
Implements the v1 REST API handlers for GoDoxy, exposing endpoints for managing routes, Docker containers, certificates, metrics, and system configuration. Implements the v1 REST API handlers for GoDoxy, exposing endpoints for managing routes, Docker containers, certificates, metrics, and system configuration.

View File

@@ -1,8 +1,6 @@
package agentapi package agentapi
import ( import (
"context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@@ -14,6 +12,7 @@ import (
config "github.com/yusing/godoxy/internal/config/types" config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/route/provider" "github.com/yusing/godoxy/internal/route/provider"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
) )
type VerifyNewAgentRequest struct { type VerifyNewAgentRequest struct {
@@ -37,9 +36,6 @@ type VerifyNewAgentRequest struct {
// @Failure 500 {object} ErrorResponse // @Failure 500 {object} ErrorResponse
// @Router /agent/verify [post] // @Router /agent/verify [post]
func Verify(c *gin.Context) { func Verify(c *gin.Context) {
// avoid timeout waiting for response headers
c.Status(http.StatusContinue)
var request VerifyNewAgentRequest var request VerifyNewAgentRequest
if err := c.ShouldBindJSON(&request); err != nil { if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
@@ -64,7 +60,7 @@ func Verify(c *gin.Context) {
return return
} }
nRoutesAdded, err := verifyNewAgent(c.Request.Context(), request.Host, ca, client, request.ContainerRuntime) nRoutesAdded, err := verifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return return
@@ -84,9 +80,9 @@ func Verify(c *gin.Context) {
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded))) c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
} }
var errAgentAlreadyExists = errors.New("agent already exists") var errAgentAlreadyExists = gperr.New("agent already exists")
func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, error) { func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
var agentCfg agent.AgentConfig var agentCfg agent.AgentConfig
agentCfg.Addr = host agentCfg.Addr = host
agentCfg.Runtime = containerRuntime agentCfg.Runtime = containerRuntime
@@ -103,14 +99,14 @@ func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client a
return 0, errAgentAlreadyExists return 0, errAgentAlreadyExists
} }
err := agentCfg.InitWithCerts(ctx, ca.Cert, client.Cert, client.Key) err := agentCfg.InitWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to initialize agent config: %w", err) return 0, gperr.Wrap(err, "failed to initialize agent config")
} }
provider := provider.NewAgentProvider(&agentCfg) provider := provider.NewAgentProvider(&agentCfg)
if _, loaded := cfgState.LoadOrStoreProvider(provider.String(), provider); loaded { if _, loaded := cfgState.LoadOrStoreProvider(provider.String(), provider); loaded {
return 0, fmt.Errorf("provider %s already exists", provider.String()) return 0, gperr.Errorf("provider %s already exists", provider.String())
} }
// agent must be added before loading routes // agent must be added before loading routes
@@ -122,7 +118,7 @@ func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client a
if err != nil { if err != nil {
cfgState.DeleteProvider(provider.String()) cfgState.DeleteProvider(provider.String())
agentpool.Remove(&agentCfg) agentpool.Remove(&agentCfg)
return 0, fmt.Errorf("failed to load routes: %w", err) return 0, gperr.Wrap(err, "failed to load routes")
} }
return provider.NumRoutes(), nil return provider.NumRoutes(), nil

View File

@@ -6,7 +6,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/autocert" "github.com/yusing/godoxy/internal/autocert"
autocertctx "github.com/yusing/godoxy/internal/autocert/types"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
) )
@@ -22,7 +21,7 @@ import (
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error" // @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
// @Router /cert/info [get] // @Router /cert/info [get]
func Info(c *gin.Context) { func Info(c *gin.Context) {
provider := autocertctx.FromCtx(c.Request.Context()) provider := autocert.ActiveProvider.Load()
if provider == nil { if provider == nil {
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled")) c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
return return

View File

@@ -6,7 +6,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
autocertctx "github.com/yusing/godoxy/internal/autocert/types" "github.com/yusing/godoxy/internal/autocert"
"github.com/yusing/godoxy/internal/logging/memlogger" "github.com/yusing/godoxy/internal/logging/memlogger"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/websocket" "github.com/yusing/goutils/http/websocket"
@@ -23,8 +23,8 @@ import (
// @Failure 500 {object} apitypes.ErrorResponse // @Failure 500 {object} apitypes.ErrorResponse
// @Router /cert/renew [get] // @Router /cert/renew [get]
func Renew(c *gin.Context) { func Renew(c *gin.Context) {
provider := autocertctx.FromCtx(c.Request.Context()) autocert := autocert.ActiveProvider.Load()
if provider == nil { if autocert == nil {
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled")) c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
return return
} }
@@ -59,7 +59,7 @@ func Renew(c *gin.Context) {
}() }()
// renewal happens in background // renewal happens in background
ok := provider.ForceExpiryAll() ok := autocert.ForceExpiryAll()
if !ok { if !ok {
log.Error().Msg("cert renewal already in progress") log.Error().Msg("cert renewal already in progress")
time.Sleep(1 * time.Second) // wait for the log above to be sent time.Sleep(1 * time.Second) // wait for the log above to be sent
@@ -67,5 +67,5 @@ func Renew(c *gin.Context) {
} }
log.Info().Msg("cert force renewal requested") log.Info().Msg("cert force renewal requested")
provider.WaitRenewalDone(manager.Context()) autocert.WaitRenewalDone(manager.Context())
} }

View File

@@ -7,7 +7,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/container"
"github.com/moby/moby/client" "github.com/moby/moby/client"
"github.com/rs/zerolog/log"
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
_ "github.com/yusing/goutils/apitypes" _ "github.com/yusing/goutils/apitypes"
@@ -37,18 +36,18 @@ func Containers(c *gin.Context) {
serveHTTP[Container](c, GetContainers) serveHTTP[Container](c, GetContainers)
} }
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, error) { func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {
errs := gperr.NewBuilder("failed to get containers") errs := gperr.NewBuilder("failed to get containers")
containers := make([]Container, 0) containers := make([]Container, 0)
for name, dockerClient := range dockerClients { for server, dockerClient := range dockerClients {
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true}) conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
if err != nil { if err != nil {
errs.AddSubject(err, name) errs.Add(err)
continue continue
} }
for _, cont := range conts.Items { for _, cont := range conts.Items {
containers = append(containers, Container{ containers = append(containers, Container{
Server: name, Server: server,
Name: cont.Names[0], Name: cont.Names[0],
ID: cont.ID, ID: cont.ID,
Image: cont.Image, Image: cont.Image,
@@ -60,10 +59,11 @@ func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Containe
return containers[i].Name < containers[j].Name return containers[i].Name < containers[j].Name
}) })
if err := errs.Error(); err != nil { if err := errs.Error(); err != nil {
if len(containers) > 0 { gperr.LogError("failed to get containers", err)
log.Err(err).Msg("failed to get containers from some servers") if len(containers) == 0 {
return containers, nil return nil, err
} }
return containers, nil
} }
return containers, errs.Error() return containers, nil
} }

View File

@@ -59,7 +59,7 @@ func Info(c *gin.Context) {
serveHTTP[dockerInfo](c, GetDockerInfo) serveHTTP[dockerInfo](c, GetDockerInfo)
} }
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, error) { func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, gperr.Error) {
errs := gperr.NewBuilder("failed to get docker info") errs := gperr.NewBuilder("failed to get docker info")
dockerInfos := make([]dockerInfo, len(dockerClients)) dockerInfos := make([]dockerInfo, len(dockerClients))
@@ -67,7 +67,7 @@ func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerIn
for name, dockerClient := range dockerClients { for name, dockerClient := range dockerClients {
info, err := dockerClient.Info(ctx, client.InfoOptions{}) info, err := dockerClient.Info(ctx, client.InfoOptions{})
if err != nil { if err != nil {
errs.AddSubject(err, name) errs.Add(err)
continue continue
} }
info.Info.Name = name info.Info.Name = name

View File

@@ -10,7 +10,7 @@ import (
"github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/container"
"github.com/moby/moby/client" "github.com/moby/moby/client"
"github.com/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/docker"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types" "github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/types" "github.com/yusing/godoxy/internal/types"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/httpheaders" "github.com/yusing/goutils/http/httpheaders"
@@ -44,7 +44,7 @@ func Stats(c *gin.Context) {
dockerCfg, ok := docker.GetDockerCfgByContainerID(id) dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
if !ok { if !ok {
var route types.Route var route types.Route
route, ok = entrypoint.FromCtx(c.Request.Context()).GetRoute(id) route, ok = routes.GetIncludeExcluded(id)
if ok { if ok {
cont := route.ContainerInfo() cont := route.ContainerInfo()
if cont == nil { if cont == nil {

View File

@@ -8,6 +8,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/docker"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/http/httpheaders" "github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket" "github.com/yusing/goutils/http/websocket"
) )
@@ -38,7 +39,7 @@ func handleResult[V any, T ResultType[V]](c *gin.Context, errs error, result T)
c.JSON(http.StatusOK, result) c.JSON(http.StatusOK, result)
} }
func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, error)) { func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
dockerClients := docker.Clients() dockerClients := docker.Clients()
defer closeAllClients(dockerClients) defer closeAllClients(dockerClients)

View File

@@ -837,45 +837,6 @@
"operationId": "stop" "operationId": "stop"
} }
}, },
"/events": {
"get": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"v1"
],
"summary": "Get events history",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Event"
}
}
},
"403": {
"description": "Forbidden: unauthorized",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
},
"500": {
"description": "Internal Server Error: internal error",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
}
},
"x-id": "events",
"operationId": "events"
}
},
"/favicon": { "/favicon": {
"get": { "get": {
"description": "Get favicon", "description": "Get favicon",
@@ -952,6 +913,7 @@
"application/json" "application/json"
], ],
"produces": [ "produces": [
"application/json",
"application/godoxy+yaml" "application/godoxy+yaml"
], ],
"tags": [ "tags": [
@@ -1257,12 +1219,6 @@
"schema": { "schema": {
"$ref": "#/definitions/ErrorResponse" "$ref": "#/definitions/ErrorResponse"
} }
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
} }
}, },
"x-id": "categories", "x-id": "categories",
@@ -1381,12 +1337,6 @@
"schema": { "schema": {
"$ref": "#/definitions/ErrorResponse" "$ref": "#/definitions/ErrorResponse"
} }
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
} }
}, },
"x-id": "items", "x-id": "items",
@@ -2870,6 +2820,43 @@
"operationId": "tail" "operationId": "tail"
} }
}, },
"/reload": {
"post": {
"description": "Reload config",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"v1"
],
"summary": "Reload config",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/SuccessResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
}
},
"x-id": "reload",
"operationId": "reload"
}
},
"/route/by_provider": { "/route/by_provider": {
"get": { "get": {
"description": "List routes by provider", "description": "List routes by provider",
@@ -2946,8 +2933,8 @@
} }
} }
}, },
"x-id": "list", "x-id": "routes",
"operationId": "list" "operationId": "routes"
} }
}, },
"/route/playground": { "/route/playground": {
@@ -3823,42 +3810,6 @@
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
"Event": {
"type": "object",
"properties": {
"action": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"category": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"data": {
"x-nullable": false,
"x-omitempty": false
},
"level": {
"$ref": "#/definitions/events.Level",
"x-nullable": false,
"x-omitempty": false
},
"timestamp": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"uuid": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
}
},
"x-nullable": false,
"x-omitempty": false
},
"FileType": { "FileType": {
"type": "string", "type": "string",
"enum": [ "enum": [
@@ -4028,6 +3979,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"latency": { "latency": {
"description": "latency in microseconds",
"type": "number", "type": "number",
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
@@ -4046,6 +3998,7 @@
"x-omitempty": false "x-omitempty": false
}, },
"uptime": { "uptime": {
"description": "uptime in milliseconds",
"type": "number", "type": "number",
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
@@ -4121,7 +4074,7 @@
"HealthMap": { "HealthMap": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"$ref": "#/definitions/HealthInfoWithoutDetail" "$ref": "#/definitions/HealthStatusString"
}, },
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
@@ -4844,19 +4797,16 @@
"properties": { "properties": {
"days": { "days": {
"type": "integer", "type": "integer",
"minimum": 0,
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
"keep_size": { "keep_size": {
"type": "integer", "type": "integer",
"minimum": 0,
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
"last": { "last": {
"type": "integer", "type": "integer",
"minimum": 0,
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
} }
@@ -5093,6 +5043,11 @@
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
"isResponseRule": {
"type": "boolean",
"x-nullable": false,
"x-omitempty": false
},
"name": { "name": {
"type": "string", "type": "string",
"x-nullable": false, "x-nullable": false,
@@ -5104,7 +5059,6 @@
"x-omitempty": false "x-omitempty": false
}, },
"validationError": { "validationError": {
"description": "we need the structured error, not the plain string",
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
} }
@@ -5140,7 +5094,6 @@
"type": "object", "type": "object",
"properties": { "properties": {
"executionError": { "executionError": {
"description": "we need the structured error, not the plain string",
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
@@ -5376,6 +5329,7 @@
"x-omitempty": false "x-omitempty": false
}, },
"bind": { "bind": {
"description": "for TCP and UDP routes, bind address to listen on",
"type": "string", "type": "string",
"x-nullable": true "x-nullable": true
}, },
@@ -6642,13 +6596,11 @@
"x-omitempty": false "x-omitempty": false
}, },
"fstype": { "fstype": {
"description": "interned",
"type": "string", "type": "string",
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
"path": { "path": {
"description": "interned",
"type": "string", "type": "string",
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
@@ -6739,23 +6691,6 @@
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
}, },
"events.Level": {
"type": "string",
"enum": [
"debug",
"info",
"warn",
"error"
],
"x-enum-varnames": [
"LevelDebug",
"LevelInfo",
"LevelWarn",
"LevelError"
],
"x-nullable": false,
"x-omitempty": false
},
"icons.Source": { "icons.Source": {
"type": "string", "type": "string",
"enum": [ "enum": [
@@ -6995,7 +6930,6 @@
"x-omitempty": false "x-omitempty": false
}, },
"name": { "name": {
"description": "interned",
"type": "string", "type": "string",
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false

View File

@@ -295,20 +295,6 @@ definitions:
message: message:
type: string type: string
type: object type: object
Event:
properties:
action:
type: string
category:
type: string
data: {}
level:
$ref: '#/definitions/events.Level'
timestamp:
type: string
uuid:
type: string
type: object
FileType: FileType:
enum: enum:
- config - config
@@ -389,6 +375,7 @@ definitions:
HealthInfoWithoutDetail: HealthInfoWithoutDetail:
properties: properties:
latency: latency:
description: latency in microseconds
type: number type: number
status: status:
enum: enum:
@@ -400,6 +387,7 @@ definitions:
- unknown - unknown
type: string type: string
uptime: uptime:
description: uptime in milliseconds
type: number type: number
type: object type: object
HealthJSON: HealthJSON:
@@ -433,7 +421,7 @@ definitions:
type: object type: object
HealthMap: HealthMap:
additionalProperties: additionalProperties:
$ref: '#/definitions/HealthInfoWithoutDetail' $ref: '#/definitions/HealthStatusString'
type: object type: object
HealthStatusString: HealthStatusString:
enum: enum:
@@ -767,13 +755,10 @@ definitions:
LogRetention: LogRetention:
properties: properties:
days: days:
minimum: 0
type: integer type: integer
keep_size: keep_size:
minimum: 0
type: integer type: integer
last: last:
minimum: 0
type: integer type: integer
type: object type: object
MetricsPeriod: MetricsPeriod:
@@ -891,12 +876,13 @@ definitions:
properties: properties:
do: do:
type: string type: string
isResponseRule:
type: boolean
name: name:
type: string type: string
"on": "on":
type: string type: string
validationError: validationError: {}
description: we need the structured error, not the plain string
type: object type: object
PlaygroundRequest: PlaygroundRequest:
properties: properties:
@@ -913,8 +899,7 @@ definitions:
type: object type: object
PlaygroundResponse: PlaygroundResponse:
properties: properties:
executionError: executionError: {}
description: we need the structured error, not the plain string
finalRequest: finalRequest:
$ref: '#/definitions/FinalRequest' $ref: '#/definitions/FinalRequest'
finalResponse: finalResponse:
@@ -1022,6 +1007,7 @@ definitions:
alias: alias:
type: string type: string
bind: bind:
description: for TCP and UDP routes, bind address to listen on
type: string type: string
x-nullable: true x-nullable: true
container: container:
@@ -1691,10 +1677,8 @@ definitions:
free: free:
type: integer type: integer
fstype: fstype:
description: interned
type: string type: string
path: path:
description: interned
type: string type: string
total: total:
type: number type: number
@@ -1762,18 +1746,6 @@ definitions:
required: required:
- id - id
type: object type: object
events.Level:
enum:
- debug
- info
- warn
- error
type: string
x-enum-varnames:
- LevelDebug
- LevelInfo
- LevelWarn
- LevelError
icons.Source: icons.Source:
enum: enum:
- https:// - https://
@@ -1889,7 +1861,6 @@ definitions:
high: high:
type: number type: number
name: name:
description: interned
type: string type: string
temperature: temperature:
type: number type: number
@@ -2481,31 +2452,6 @@ paths:
tags: tags:
- docker - docker
x-id: stop x-id: stop
/events:
get:
consumes:
- application/json
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/Event'
type: array
"403":
description: 'Forbidden: unauthorized'
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: 'Internal Server Error: internal error'
schema:
$ref: '#/definitions/ErrorResponse'
summary: Get events history
tags:
- v1
x-id: events
/favicon: /favicon:
get: get:
consumes: consumes:
@@ -2576,6 +2522,7 @@ paths:
- FileTypeProvider - FileTypeProvider
- FileTypeMiddleware - FileTypeMiddleware
produces: produces:
- application/json
- application/godoxy+yaml - application/godoxy+yaml
responses: responses:
"200": "200":
@@ -2760,10 +2707,6 @@ paths:
description: Forbidden description: Forbidden
schema: schema:
$ref: '#/definitions/ErrorResponse' $ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: List homepage categories summary: List homepage categories
tags: tags:
- homepage - homepage
@@ -2841,10 +2784,6 @@ paths:
description: Forbidden description: Forbidden
schema: schema:
$ref: '#/definitions/ErrorResponse' $ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Homepage items summary: Homepage items
tags: tags:
- homepage - homepage
@@ -3851,6 +3790,30 @@ paths:
- proxmox - proxmox
- websocket - websocket
x-id: tail x-id: tail
/reload:
post:
consumes:
- application/json
description: Reload config
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/SuccessResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Reload config
tags:
- v1
x-id: reload
/route/{which}: /route/{which}:
get: get:
consumes: consumes:
@@ -3936,7 +3899,7 @@ paths:
tags: tags:
- route - route
- websocket - websocket
x-id: list x-id: routes
/route/playground: /route/playground:
post: post:
consumes: consumes:

View File

@@ -1,44 +0,0 @@
package v1
import (
"context"
"errors"
"net/http"
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/events"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
)
// @x-id "events"
// @BasePath /api/v1
// @Summary Get events history
// @Tags v1
// @Accept json
// @Produce json
// @Success 200 {array} events.Event
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden: unauthorized"
// @Failure 500 {object} apitypes.ErrorResponse "Internal Server Error: internal error"
// @Router /events [get]
func Events(c *gin.Context) {
if !httpheaders.IsWebsocket(c.Request.Header) {
c.JSON(http.StatusOK, events.Global.Get())
return
}
manager, err := websocket.NewManagerWithUpgrade(c)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
return
}
defer manager.Close()
writer := manager.NewWriter(websocket.TextMessage)
err = events.Global.ListenJSON(c.Request.Context(), writer)
if err != nil && !errors.Is(err, context.Canceled) {
c.Error(apitypes.InternalServerError(err, "failed to listen to events"))
return
}
}

View File

@@ -5,9 +5,9 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/homepage/icons" "github.com/yusing/godoxy/internal/homepage/icons"
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch" iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
"github.com/yusing/godoxy/internal/route/routes"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
_ "unsafe" _ "unsafe"
@@ -73,11 +73,7 @@ func FavIcon(c *gin.Context) {
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias //go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) { func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) {
// try with route.Icon // try with route.Icon
ep := entrypoint.FromCtx(ctx) r, ok := routes.HTTP.Get(alias)
if ep == nil { // impossible, but just in case
return iconfetch.FetchResultWithErrorf(http.StatusInternalServerError, "entrypoint not initialized")
}
r, ok := ep.HTTPRoutes().Get(alias)
if !ok { if !ok {
return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found") return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
} }

View File

@@ -30,7 +30,7 @@ type GetFileContentRequest struct {
// @Description Get file content // @Description Get file content
// @Tags file // @Tags file
// @Accept json // @Accept json
// @Produce application/godoxy+yaml // @Produce json,application/godoxy+yaml
// @Param query query GetFileContentRequest true "Request" // @Param query query GetFileContentRequest true "Request"
// @Success 200 {string} application/godoxy+yaml "File content" // @Success 200 {string} application/godoxy+yaml "File content"
// @Failure 400 {object} apitypes.ErrorResponse // @Failure 400 {object} apitypes.ErrorResponse

View File

@@ -51,7 +51,7 @@ func Validate(c *gin.Context) {
c.JSON(http.StatusOK, apitypes.Success("file validated")) c.JSON(http.StatusOK, apitypes.Success("file validated"))
} }
func validateFile(fileType FileType, content []byte) error { func validateFile(fileType FileType, content []byte) gperr.Error {
switch fileType { switch fileType {
case FileTypeConfig: case FileTypeConfig:
return config.Validate(content) return config.Validate(content)

View File

@@ -5,14 +5,12 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types" "github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/httpheaders" "github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket" "github.com/yusing/goutils/http/websocket"
)
type HealthMap = map[string]types.HealthInfoWithoutDetail // @name HealthMap _ "github.com/yusing/goutils/apitypes"
)
// @x-id "health" // @x-id "health"
// @BasePath /api/v1 // @BasePath /api/v1
@@ -21,21 +19,16 @@ type HealthMap = map[string]types.HealthInfoWithoutDetail // @name HealthMap
// @Tags v1,websocket // @Tags v1,websocket
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} HealthMap "Health info by route name" // @Success 200 {object} routes.HealthMap "Health info by route name"
// @Failure 403 {object} apitypes.ErrorResponse // @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse // @Failure 500 {object} apitypes.ErrorResponse
// @Router /health [get] // @Router /health [get]
func Health(c *gin.Context) { func Health(c *gin.Context) {
ep := entrypoint.FromCtx(c.Request.Context())
if ep == nil { // impossible, but just in case
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
return
}
if httpheaders.IsWebsocket(c.Request.Header) { if httpheaders.IsWebsocket(c.Request.Header) {
websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) { websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) {
return ep.GetHealthInfoWithoutDetail(), nil return routes.GetHealthInfoSimple(), nil
}) })
} else { } else {
c.JSON(http.StatusOK, ep.GetHealthInfoWithoutDetail()) c.JSON(http.StatusOK, routes.GetHealthInfoSimple())
} }
} }

View File

@@ -4,10 +4,10 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/homepage" "github.com/yusing/godoxy/internal/homepage"
"github.com/yusing/godoxy/internal/route/routes"
apitypes "github.com/yusing/goutils/apitypes" _ "github.com/yusing/goutils/apitypes"
) )
// @x-id "categories" // @x-id "categories"
@@ -19,23 +19,17 @@ import (
// @Produce json // @Produce json
// @Success 200 {array} string // @Success 200 {array} string
// @Failure 403 {object} apitypes.ErrorResponse // @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/categories [get] // @Router /homepage/categories [get]
func Categories(c *gin.Context) { func Categories(c *gin.Context) {
ep := entrypoint.FromCtx(c.Request.Context()) c.JSON(http.StatusOK, HomepageCategories())
if ep == nil { // impossible, but just in case
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
return
}
c.JSON(http.StatusOK, HomepageCategories(ep))
} }
func HomepageCategories(ep entrypoint.Entrypoint) []string { func HomepageCategories() []string {
check := make(map[string]struct{}) check := make(map[string]struct{})
categories := make([]string, 0) categories := make([]string, 0)
categories = append(categories, homepage.CategoryAll) categories = append(categories, homepage.CategoryAll)
categories = append(categories, homepage.CategoryFavorites) categories = append(categories, homepage.CategoryFavorites)
for _, r := range ep.HTTPRoutes().Iter { for _, r := range routes.HTTP.Iter {
item := r.HomepageItem() item := r.HomepageItem()
if item.Category == "" { if item.Category == "" {
continue continue

View File

@@ -10,8 +10,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lithammer/fuzzysearch/fuzzy" "github.com/lithammer/fuzzysearch/fuzzy"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/homepage" "github.com/yusing/godoxy/internal/homepage"
"github.com/yusing/godoxy/internal/route/routes"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/httpheaders" "github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket" "github.com/yusing/goutils/http/websocket"
@@ -36,7 +36,6 @@ type HomepageItemsRequest struct {
// @Success 200 {object} homepage.Homepage // @Success 200 {object} homepage.Homepage
// @Failure 400 {object} apitypes.ErrorResponse // @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse // @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/items [get] // @Router /homepage/items [get]
func Items(c *gin.Context) { func Items(c *gin.Context) {
var request HomepageItemsRequest var request HomepageItemsRequest
@@ -54,35 +53,29 @@ func Items(c *gin.Context) {
hostname = host hostname = host
} }
ep := entrypoint.FromCtx(c.Request.Context())
if ep == nil {
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not found in context", nil))
return
}
if httpheaders.IsWebsocket(c.Request.Header) { if httpheaders.IsWebsocket(c.Request.Header) {
websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) { websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) {
return HomepageItems(ep, proto, hostname, &request), nil return HomepageItems(proto, hostname, &request), nil
}) })
} else { } else {
c.JSON(http.StatusOK, HomepageItems(ep, proto, hostname, &request)) c.JSON(http.StatusOK, HomepageItems(proto, hostname, &request))
} }
} }
func HomepageItems(ep entrypoint.Entrypoint, proto, hostname string, request *HomepageItemsRequest) homepage.Homepage { func HomepageItems(proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
switch proto { switch proto {
case "http", "https": case "http", "https":
default: default:
proto = "http" proto = "http"
} }
hp := homepage.NewHomepageMap(ep.HTTPRoutes().Size()) hp := homepage.NewHomepageMap(routes.HTTP.Size())
if strings.Count(hostname, ".") > 1 { if strings.Count(hostname, ".") > 1 {
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain _, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
} }
for _, r := range ep.HTTPRoutes().Iter { for _, r := range routes.HTTP.Iter {
if request.Provider != "" && r.ProviderName() != request.Provider { if request.Provider != "" && r.ProviderName() != request.Provider {
continue continue
} }

View File

@@ -4,12 +4,10 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/url"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
"github.com/cenkalti/backoff/v5"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent" "github.com/yusing/godoxy/agent/pkg/agent"
@@ -37,11 +35,6 @@ type bytesFromPool struct {
release func([]byte) release func([]byte)
} }
type systemInfoData struct {
agentName string
systemInfo any
}
// @x-id "all_system_info" // @x-id "all_system_info"
// @BasePath /api/v1 // @BasePath /api/v1
// @Summary Get system info // @Summary Get system info
@@ -79,19 +72,91 @@ func AllSystemInfo(c *gin.Context) {
defer manager.Close() defer manager.Close()
query := c.Request.URL.Query() query := c.Request.URL.Query()
queryEncoded := query.Encode() queryEncoded := c.Request.URL.Query().Encode()
type SystemInfoData struct {
AgentName string
SystemInfo any
}
// leave 5 extra slots for buffering in case new agents are added. // leave 5 extra slots for buffering in case new agents are added.
dataCh := make(chan systemInfoData, 1+agentpool.Num()+5) dataCh := make(chan SystemInfoData, 1+agentpool.Num()+5)
defer close(dataCh)
ticker := time.NewTicker(req.Interval) ticker := time.NewTicker(req.Interval)
defer ticker.Stop() defer ticker.Stop()
go streamSystemInfo(manager, dataCh) go func() {
for {
select {
case <-manager.Done():
return
case data := <-dataCh:
err := marshalSystemInfo(manager, data.AgentName, data.SystemInfo)
if err != nil {
manager.Close()
return
}
}
}
}()
// processing function for one round.
doRound := func() (bool, error) {
var numErrs atomic.Int32
totalAgents := int32(1) // myself
var errs gperr.Group
// get system info for me and all agents in parallel.
errs.Go(func() error {
data, err := systeminfo.Poller.GetRespData(req.Period, query)
if err != nil {
numErrs.Add(1)
return gperr.PrependSubject("Main server", err)
}
select {
case <-manager.Done():
return nil
case dataCh <- SystemInfoData{
AgentName: "GoDoxy",
SystemInfo: data,
}:
}
return nil
})
for _, a := range agentpool.Iter() {
totalAgents++
errs.Go(func() error {
data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded)
if err != nil {
numErrs.Add(1)
return gperr.PrependSubject("Agent "+a.Name, err)
}
select {
case <-manager.Done():
return nil
case dataCh <- SystemInfoData{
AgentName: a.Name,
SystemInfo: data,
}:
}
return nil
})
}
err := errs.Wait().Error()
return numErrs.Load() == totalAgents, err
}
// write system info immediately once. // write system info immediately once.
if hasSuccess, err := collectSystemInfoRound(manager, req, query, queryEncoded, dataCh); handleRoundResult(c, hasSuccess, err, false) { if shouldContinue, err := doRound(); err != nil {
return if !shouldContinue {
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
return
}
} }
// then continue on the ticker. // then continue on the ticker.
@@ -100,95 +165,17 @@ func AllSystemInfo(c *gin.Context) {
case <-manager.Done(): case <-manager.Done():
return return
case <-ticker.C: case <-ticker.C:
if hasSuccess, err := collectSystemInfoRound(manager, req, query, queryEncoded, dataCh); handleRoundResult(c, hasSuccess, err, true) { if shouldContinue, err := doRound(); err != nil {
return if !shouldContinue {
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
return
}
gperr.LogWarn("failed to get some system info", err)
} }
} }
} }
} }
func streamSystemInfo(manager *websocket.Manager, dataCh <-chan systemInfoData) {
for {
select {
case <-manager.Done():
return
case data := <-dataCh:
err := marshalSystemInfo(manager, data.agentName, data.systemInfo)
if err != nil {
manager.Close()
return
}
}
}
}
func queueSystemInfo(manager *websocket.Manager, dataCh chan<- systemInfoData, data systemInfoData) {
select {
case <-manager.Done():
case dataCh <- data:
}
}
func collectSystemInfoRound(
manager *websocket.Manager,
req AllSystemInfoRequest,
query url.Values,
queryEncoded string,
dataCh chan<- systemInfoData,
) (hasSuccess bool, err error) {
var numErrs atomic.Int32
totalAgents := int32(1) // myself
var errs gperr.Group
// get system info for me and all agents in parallel.
errs.Go(func() error {
data, err := systeminfo.Poller.GetRespData(req.Period, query)
if err != nil {
numErrs.Add(1)
return gperr.PrependSubject(err, "Main server")
}
queueSystemInfo(manager, dataCh, systemInfoData{
agentName: "GoDoxy",
systemInfo: data,
})
return nil
})
for _, a := range agentpool.Iter() {
totalAgents++
errs.Go(func() error {
data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded)
if err != nil {
numErrs.Add(1)
return gperr.PrependSubject(err, "Agent "+a.Name)
}
queueSystemInfo(manager, dataCh, systemInfoData{
agentName: a.Name,
systemInfo: data,
})
return nil
})
}
err = errs.Wait().Error()
return numErrs.Load() < totalAgents, err
}
func handleRoundResult(c *gin.Context, hasSuccess bool, err error, logPartial bool) (stop bool) {
if err == nil {
return false
}
if !hasSuccess {
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
return true
}
if logPartial {
log.Warn().Err(err).Msg("failed to get some system info")
}
return false
}
func getAgentSystemInfo(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) { func getAgentSystemInfo(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() defer cancel()
@@ -210,26 +197,35 @@ func getAgentSystemInfo(ctx context.Context, a *agentpool.Agent, query string) (
func getAgentSystemInfoWithRetry(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) { func getAgentSystemInfoWithRetry(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) {
const maxRetries = 3 const maxRetries = 3
const retryDelay = 5 * time.Second var lastErr error
var attempt int
data, err := backoff.Retry(ctx, func() (bytesFromPool, error) { for attempt := range maxRetries {
attempt++ // Apply backoff delay for retries (not for first attempt)
if attempt > 0 {
delay := max((1<<attempt)*time.Second, 5*time.Second)
select {
case <-ctx.Done():
return bytesFromPool{}, ctx.Err()
case <-time.After(delay):
}
}
data, err := getAgentSystemInfo(ctx, a, query) data, err := getAgentSystemInfo(ctx, a, query)
if err == nil { if err == nil {
return data, nil return data, nil
} }
log.Err(err).Str("agent", a.Name).Int("attempt", attempt).Msg("Agent request attempt failed") lastErr = err
return bytesFromPool{}, err
}, log.Debug().Str("agent", a.Name).Int("attempt", attempt+1).Str("error", err.Error()).Msg("Agent request attempt failed")
backoff.WithBackOff(backoff.NewConstantBackOff(retryDelay)),
backoff.WithMaxTries(maxRetries), // Don't retry on context cancellation
) if ctx.Err() != nil {
if err != nil { return bytesFromPool{}, ctx.Err()
return bytesFromPool{}, err }
} }
return data, nil
return bytesFromPool{}, lastErr
} }
func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any) error { func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any) error {

View File

@@ -2,5 +2,5 @@ package proxmoxapi
type ActionRequest struct { type ActionRequest struct {
Node string `uri:"node" binding:"required"` Node string `uri:"node" binding:"required"`
VMID uint64 `uri:"vmid" binding:"required"` VMID int `uri:"vmid" binding:"required"`
} // @name ProxmoxVMActionRequest } // @name ProxmoxVMActionRequest

View File

@@ -8,7 +8,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/proxmox" "github.com/yusing/godoxy/internal/proxmox"
"github.com/yusing/goutils/apitypes" "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket" "github.com/yusing/goutils/http/websocket"
) )
@@ -16,10 +15,10 @@ import (
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl/pve/127?service=pveproxy&service=pvedaemon&limit=10 // e.g. ws://localhost:8889/api/v1/proxmox/journalctl/pve/127?service=pveproxy&service=pvedaemon&limit=10
type JournalctlRequest struct { type JournalctlRequest struct {
Node string `form:"node" uri:"node" binding:"required"` // Node name Node string `form:"node" uri:"node" binding:"required"` // Node name
VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl) VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
Services []string `form:"service" uri:"service"` // Service names Services []string `form:"service" uri:"service"` // Service names
Limit *int `form:"limit" uri:"limit" default:"100" binding:"omitempty,min=1,max=1000"` // Limit output lines (1-1000) Limit *int `form:"limit" uri:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
} // @name ProxmoxJournalctlRequest } // @name ProxmoxJournalctlRequest
// @x-id "journalctl" // @x-id "journalctl"
@@ -41,11 +40,6 @@ type JournalctlRequest struct {
// @Router /proxmox/journalctl/{node}/{vmid} [get] // @Router /proxmox/journalctl/{node}/{vmid} [get]
// @Router /proxmox/journalctl/{node}/{vmid}/{service} [get] // @Router /proxmox/journalctl/{node}/{vmid}/{service} [get]
func Journalctl(c *gin.Context) { func Journalctl(c *gin.Context) {
if !httpheaders.IsWebsocket(c.Request.Header) {
c.JSON(http.StatusBadRequest, apitypes.Error("websocket required"))
return
}
var request JournalctlRequest var request JournalctlRequest
uriErr := c.ShouldBindUri(&request) uriErr := c.ShouldBindUri(&request)
queryErr := c.ShouldBindQuery(&request) queryErr := c.ShouldBindQuery(&request)
@@ -54,10 +48,6 @@ func Journalctl(c *gin.Context) {
return return
} }
if request.Limit == nil {
request.Limit = new(100)
}
node, ok := proxmox.Nodes.Get(request.Node) node, ok := proxmox.Nodes.Get(request.Node)
if !ok { if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("node not found")) c.JSON(http.StatusNotFound, apitypes.Error("node not found"))

View File

@@ -11,7 +11,10 @@ import (
"github.com/yusing/goutils/http/websocket" "github.com/yusing/goutils/http/websocket"
) )
type StatsRequest ActionRequest type StatsRequest struct {
Node string `uri:"node" binding:"required"`
VMID int `uri:"vmid" binding:"required"`
}
// @x-id "nodeStats" // @x-id "nodeStats"
// @BasePath /api/v1 // @BasePath /api/v1

View File

@@ -7,7 +7,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/proxmox" "github.com/yusing/godoxy/internal/proxmox"
"github.com/yusing/goutils/apitypes" "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket" "github.com/yusing/goutils/http/websocket"
) )
@@ -35,11 +34,6 @@ type TailRequest struct {
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error" // @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
// @Router /proxmox/tail [get] // @Router /proxmox/tail [get]
func Tail(c *gin.Context) { func Tail(c *gin.Context) {
if !httpheaders.IsWebsocket(c.Request.Header) {
c.JSON(http.StatusBadRequest, apitypes.Error("websocket required"))
return
}
var request TailRequest var request TailRequest
if err := c.ShouldBindQuery(&request); err != nil { if err := c.ShouldBindQuery(&request); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))

28
internal/api/v1/reload.go Normal file
View File

@@ -0,0 +1,28 @@
package v1
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/config"
apitypes "github.com/yusing/goutils/apitypes"
)
// @x-id "reload"
// @BasePath /api/v1
// @Summary Reload config
// @Description Reload config
// @Tags v1
// @Accept json
// @Produce json
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /reload [post]
func Reload(c *gin.Context) {
if err := config.Reload(); err != nil {
c.Error(apitypes.InternalServerError(err, "failed to reload config"))
return
}
c.JSON(http.StatusOK, apitypes.Success("config reloaded"))
}

View File

@@ -4,10 +4,10 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/route" "github.com/yusing/godoxy/internal/route"
"github.com/yusing/godoxy/internal/route/routes"
apitypes "github.com/yusing/goutils/apitypes" _ "github.com/yusing/goutils/apitypes"
) )
type RoutesByProvider map[string][]route.Route type RoutesByProvider map[string][]route.Route
@@ -24,10 +24,5 @@ type RoutesByProvider map[string][]route.Route
// @Failure 500 {object} apitypes.ErrorResponse // @Failure 500 {object} apitypes.ErrorResponse
// @Router /route/by_provider [get] // @Router /route/by_provider [get]
func ByProvider(c *gin.Context) { func ByProvider(c *gin.Context) {
ep := entrypoint.FromCtx(c.Request.Context()) c.JSON(http.StatusOK, routes.ByProvider())
if ep == nil { // impossible, but just in case
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
return
}
c.JSON(http.StatusOK, ep.RoutesByProvider())
} }

View File

@@ -1,7 +1,6 @@
package routeApi package routeApi
import ( import (
"fmt"
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@@ -55,15 +54,16 @@ type PlaygroundResponse struct {
MatchedRules []string `json:"matchedRules"` MatchedRules []string `json:"matchedRules"`
FinalRequest FinalRequest `json:"finalRequest"` FinalRequest FinalRequest `json:"finalRequest"`
FinalResponse FinalResponse `json:"finalResponse"` FinalResponse FinalResponse `json:"finalResponse"`
ExecutionError error `json:"executionError,omitempty"` // we need the structured error, not the plain string ExecutionError gperr.Error `json:"executionError,omitempty"`
UpstreamCalled bool `json:"upstreamCalled"` UpstreamCalled bool `json:"upstreamCalled"`
} // @name PlaygroundResponse } // @name PlaygroundResponse
type ParsedRule struct { type ParsedRule struct {
Name string `json:"name"` Name string `json:"name"`
On string `json:"on"` On string `json:"on"`
Do string `json:"do"` Do string `json:"do"`
ValidationError error `json:"validationError,omitempty"` // we need the structured error, not the plain string ValidationError gperr.Error `json:"validationError,omitempty"`
IsResponseRule bool `json:"isResponseRule"`
} // @name ParsedRule } // @name ParsedRule
type FinalRequest struct { type FinalRequest struct {
@@ -138,7 +138,7 @@ func Playground(c *gin.Context) {
// Execute rules // Execute rules
matchedRules := []string{} matchedRules := []string{}
upstreamCalled := false upstreamCalled := false
var executionError error var executionError gperr.Error
// Variables to capture modified request state // Variables to capture modified request state
var finalReqMethod, finalReqPath, finalReqHost string var finalReqMethod, finalReqPath, finalReqHost string
@@ -244,22 +244,20 @@ func Playground(c *gin.Context) {
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} }
func handlerWithRecover(w http.ResponseWriter, r *http.Request, h http.HandlerFunc, outErr *error) { func handlerWithRecover(w http.ResponseWriter, r *http.Request, h http.HandlerFunc, outErr *gperr.Error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
if outErr != nil { if outErr != nil {
*outErr = fmt.Errorf("panic during rule execution: %v", r) *outErr = gperr.Errorf("panic during rule execution: %v", r)
} }
} }
}() }()
h(w, r) h(w, r)
} }
func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) { func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, gperr.Error) {
parsedRules := make([]ParsedRule, 0, len(rawRules)) var parsedRules []ParsedRule
rulesList := make(rules.Rules, 0, len(rawRules)) var rulesList rules.Rules
var valErrs gperr.Builder
// Parse each rule individually to capture per-rule errors // Parse each rule individually to capture per-rule errors
for _, rawRule := range rawRules { for _, rawRule := range rawRules {
@@ -286,17 +284,14 @@ func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
// Determine if valid // Determine if valid
isValid := onErr == nil && doErr == nil isValid := onErr == nil && doErr == nil
var validationErr error validationErr := gperr.Join(gperr.PrependSubject("on", onErr), gperr.PrependSubject("do", doErr))
if !isValid {
validationErr = gperr.Join(gperr.PrependSubject(onErr, "on"), gperr.PrependSubject(doErr, "do"))
valErrs.Add(validationErr)
}
parsedRules = append(parsedRules, ParsedRule{ parsedRules = append(parsedRules, ParsedRule{
Name: name, Name: name,
On: onStr, On: onStr,
Do: doStr, Do: doStr,
ValidationError: validationErr, ValidationError: validationErr,
IsResponseRule: rule.IsResponseRule(),
}) })
// Only add valid rules to execution list // Only add valid rules to execution list
@@ -305,7 +300,7 @@ func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
} }
} }
return parsedRules, rulesList, valErrs.Error() return parsedRules, rulesList, nil
} }
func createMockRequest(mock MockRequest) *http.Request { func createMockRequest(mock MockRequest) *http.Request {

View File

@@ -79,7 +79,7 @@ func TestPlayground(t *testing.T) {
if len(resp.MatchedRules) != 1 { if len(resp.MatchedRules) != 1 {
t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules)) t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules))
} }
if resp.FinalResponse.StatusCode != http.StatusForbidden { if resp.FinalResponse.StatusCode != 403 {
t.Errorf("expected status 403, got %d", resp.FinalResponse.StatusCode) t.Errorf("expected status 403, got %d", resp.FinalResponse.StatusCode)
} }
if resp.UpstreamCalled { if resp.UpstreamCalled {
@@ -168,7 +168,7 @@ func TestPlayground(t *testing.T) {
if len(resp.MatchedRules) != 1 { if len(resp.MatchedRules) != 1 {
t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules)) t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules))
} }
if resp.FinalResponse.StatusCode != http.StatusMethodNotAllowed { if resp.FinalResponse.StatusCode != 405 {
t.Errorf("expected status 405, got %d", resp.FinalResponse.StatusCode) t.Errorf("expected status 405, got %d", resp.FinalResponse.StatusCode)
} }
}, },
@@ -179,7 +179,7 @@ func TestPlayground(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Create request // Create request
body, _ := json.Marshal(tt.request) body, _ := json.Marshal(tt.request)
req := httptest.NewRequest(http.MethodPost, "/api/v1/route/playground", bytes.NewReader(body)) req := httptest.NewRequest("POST", "/api/v1/route/playground", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
// Create response recorder // Create response recorder
@@ -214,7 +214,7 @@ func TestPlayground(t *testing.T) {
func TestPlaygroundInvalidRequest(t *testing.T) { func TestPlaygroundInvalidRequest(t *testing.T) {
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
req := httptest.NewRequest(http.MethodPost, "/api/v1/route/playground", bytes.NewReader([]byte(`{}`))) req := httptest.NewRequest("POST", "/api/v1/route/playground", bytes.NewReader([]byte(`{}`)))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder() w := httptest.NewRecorder()

View File

@@ -4,7 +4,7 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types" "github.com/yusing/godoxy/internal/route/routes"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
) )
@@ -32,13 +32,7 @@ func Route(c *gin.Context) {
return return
} }
ep := entrypoint.FromCtx(c.Request.Context()) route, ok := routes.GetIncludeExcluded(request.Which)
if ep == nil { // impossible, but just in case
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
return
}
route, ok := ep.GetRoute(request.Which)
if ok { if ok {
c.JSON(http.StatusOK, route) c.JSON(http.StatusOK, route)
return return

View File

@@ -6,8 +6,8 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/route" "github.com/yusing/godoxy/internal/route"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/types" "github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/http/httpheaders" "github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket" "github.com/yusing/goutils/http/websocket"
@@ -15,7 +15,7 @@ import (
type RouteType route.Route // @name Route type RouteType route.Route // @name Route
// @x-id "list" // @x-id "routes"
// @BasePath /api/v1 // @BasePath /api/v1
// @Summary List routes // @Summary List routes
// @Description List routes // @Description List routes
@@ -32,16 +32,14 @@ func Routes(c *gin.Context) {
return return
} }
ep := entrypoint.FromCtx(c.Request.Context())
provider := c.Query("provider") provider := c.Query("provider")
if provider == "" { if provider == "" {
c.JSON(http.StatusOK, slices.Collect(ep.IterRoutes)) c.JSON(http.StatusOK, slices.Collect(routes.IterAll))
return return
} }
rts := make([]types.Route, 0, ep.NumRoutes()) rts := make([]types.Route, 0, routes.NumAllRoutes())
for r := range ep.IterRoutes { for r := range routes.IterAll {
if r.ProviderName() == provider { if r.ProviderName() == provider {
rts = append(rts, r) rts = append(rts, r)
} }
@@ -50,19 +48,17 @@ func Routes(c *gin.Context) {
} }
func RoutesWS(c *gin.Context) { func RoutesWS(c *gin.Context) {
ep := entrypoint.FromCtx(c.Request.Context())
provider := c.Query("provider") provider := c.Query("provider")
if provider == "" { if provider == "" {
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) { websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
return slices.Collect(ep.IterRoutes), nil return slices.Collect(routes.IterAll), nil
}) })
return return
} }
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) { websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
rts := make([]types.Route, 0, ep.NumRoutes()) rts := make([]types.Route, 0, routes.NumAllRoutes())
for r := range ep.IterRoutes { for r := range routes.IterAll {
if r.ProviderName() == provider { if r.ProviderName() == provider {
rts = append(rts, r) rts = append(rts, r)
} }

View File

@@ -1,4 +1,4 @@
# internal/auth # Authentication
Authentication providers supporting OIDC and username/password authentication with JWT-based sessions. Authentication providers supporting OIDC and username/password authentication with JWT-based sessions.

View File

@@ -135,7 +135,7 @@ func (auth *OIDCProvider) setSessionTokenCookie(w http.ResponseWriter, r *http.R
func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionClaims, valid bool, err error) { func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionClaims, valid bool, err error) {
claims = &sessionClaims{} claims = &sessionClaims{}
sessionToken, err := jwt.ParseWithClaims(sessionJWT, claims, func(t *jwt.Token) (any, error) { sessionToken, err := jwt.ParseWithClaims(sessionJWT, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
} }

View File

@@ -17,6 +17,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common" "github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils" "github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http" httputils "github.com/yusing/goutils/http"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/time/rate" "golang.org/x/time/rate"
@@ -75,8 +76,8 @@ const (
var ( var (
errMissingIDToken = errors.New("missing id_token field from oauth token") errMissingIDToken = errors.New("missing id_token field from oauth token")
ErrMissingOAuthToken = errors.New("missing oauth token") ErrMissingOAuthToken = gperr.New("missing oauth token")
ErrInvalidOAuthToken = errors.New("invalid oauth token") ErrInvalidOAuthToken = gperr.New("invalid oauth token")
) )
// generateState generates a random string for OIDC state. // generateState generates a random string for OIDC state.

View File

@@ -1,7 +1,6 @@
package auth package auth
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
@@ -9,12 +8,16 @@ import (
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/yusing/godoxy/internal/common" "github.com/yusing/godoxy/internal/common"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http" httputils "github.com/yusing/goutils/http"
strutils "github.com/yusing/goutils/strings" strutils "github.com/yusing/goutils/strings"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
var ErrInvalidUsername = errors.New("invalid username") var (
ErrInvalidUsername = gperr.New("invalid username")
ErrInvalidPassword = gperr.New("invalid password")
)
type ( type (
UserPassAuth struct { UserPassAuth struct {
@@ -24,9 +27,8 @@ type (
tokenTTL time.Duration tokenTTL time.Duration
} }
UserPassClaims struct { UserPassClaims struct {
jwt.RegisteredClaims
Username string `json:"username"` Username string `json:"username"`
jwt.RegisteredClaims
} }
) )
@@ -79,7 +81,7 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
return ErrMissingSessionToken return ErrMissingSessionToken
} }
var claims UserPassClaims var claims UserPassClaims
token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (any, error) { token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
} }
@@ -92,9 +94,9 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
case !token.Valid: case !token.Valid:
return ErrInvalidSessionToken return ErrInvalidSessionToken
case claims.Username != auth.username: case claims.Username != auth.username:
return fmt.Errorf("%w: %s", ErrUserNotAllowed, claims.Username) return ErrUserNotAllowed.Subject(claims.Username)
case claims.ExpiresAt.Before(time.Now()): case claims.ExpiresAt.Before(time.Now()):
return fmt.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time)) return gperr.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
} }
return nil return nil
@@ -137,12 +139,11 @@ func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request)
} }
func (auth *UserPassAuth) validatePassword(user, pass string) error { func (auth *UserPassAuth) validatePassword(user, pass string) error {
// always perform bcrypt comparison to avoid timing attacks
if err := bcrypt.CompareHashAndPassword(auth.pwdHash, []byte(pass)); err != nil {
return err
}
if user != auth.username { if user != auth.username {
return ErrInvalidUsername return ErrInvalidUsername.Subject(user)
}
if err := bcrypt.CompareHashAndPassword(auth.pwdHash, []byte(pass)); err != nil {
return ErrInvalidPassword.With(err).Subject(pass)
} }
return nil return nil
} }

View File

@@ -27,7 +27,7 @@ func TestUserPassValidateCredentials(t *testing.T) {
err := auth.validatePassword("username", "password") err := auth.validatePassword("username", "password")
expect.NoError(t, err) expect.NoError(t, err)
err = auth.validatePassword("username", "wrong-password") err = auth.validatePassword("username", "wrong-password")
expect.ErrorIs(t, bcrypt.ErrMismatchedHashAndPassword, err) expect.ErrorIs(t, ErrInvalidPassword, err)
err = auth.validatePassword("wrong-username", "password") err = auth.validatePassword("wrong-username", "password")
expect.ErrorIs(t, ErrInvalidUsername, err) expect.ErrorIs(t, ErrInvalidUsername, err)
} }

View File

@@ -1,19 +1,20 @@
package auth package auth
import ( import (
"errors"
"net" "net"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/yusing/godoxy/internal/common" "github.com/yusing/godoxy/internal/common"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
) )
var ( var (
ErrMissingSessionToken = errors.New("missing session token") ErrMissingSessionToken = gperr.New("missing session token")
ErrInvalidSessionToken = errors.New("invalid session token") ErrInvalidSessionToken = gperr.New("invalid session token")
ErrUserNotAllowed = errors.New("user not allowed") ErrUserNotAllowed = gperr.New("user not allowed")
) )
func IsFrontend(r *http.Request) bool { func IsFrontend(r *http.Request) bool {
@@ -69,12 +70,12 @@ func cookieDomain(r *http.Request) string {
} }
} }
parts := strings.Split(reqHost, ".") parts := strutils.SplitRune(reqHost, '.')
if len(parts) < 2 { if len(parts) < 2 {
return "" return ""
} }
parts[0] = "" parts[0] = ""
return strings.Join(parts, ".") return strutils.JoinRune(parts, '.')
} }
func SetTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) { func SetTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {

View File

@@ -1,4 +1,4 @@
# internal/autocert # Autocert Package
Automated SSL certificate management using the ACME protocol (Let's Encrypt and compatible CAs). Automated SSL certificate management using the ACME protocol (Let's Encrypt and compatible CAs).

View File

@@ -23,35 +23,33 @@ import (
strutils "github.com/yusing/goutils/strings" strutils "github.com/yusing/goutils/strings"
) )
type ( type ConfigExtra Config
ConfigExtra Config type Config struct {
Config struct { Email string `json:"email,omitempty"`
Email string `json:"email,omitempty"` Domains []string `json:"domains,omitempty"`
Domains []string `json:"domains,omitempty"` CertPath string `json:"cert_path,omitempty"`
CertPath string `json:"cert_path,omitempty"` KeyPath string `json:"key_path,omitempty"`
KeyPath string `json:"key_path,omitempty"` Extra []ConfigExtra `json:"extra,omitempty"`
Extra []ConfigExtra `json:"extra,omitempty"` ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers with the same CA directory URL
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers with the same CA directory URL Provider string `json:"provider,omitempty"`
Provider string `json:"provider,omitempty"` Options map[string]strutils.Redacted `json:"options,omitempty"`
Options map[string]strutils.Redacted `json:"options,omitempty"`
Resolvers []string `json:"resolvers,omitempty"` Resolvers []string `json:"resolvers,omitempty"`
// Custom ACME CA // Custom ACME CA
CADirURL string `json:"ca_dir_url,omitempty"` CADirURL string `json:"ca_dir_url,omitempty"`
CACerts []string `json:"ca_certs,omitempty"` CACerts []string `json:"ca_certs,omitempty"`
// EAB // EAB
EABKid string `json:"eab_kid,omitempty" validate:"required_with=EABHmac"` EABKid string `json:"eab_kid,omitempty" validate:"required_with=EABHmac"`
EABHmac string `json:"eab_hmac,omitempty" validate:"required_with=EABKid"` // base64 encoded EABHmac string `json:"eab_hmac,omitempty" validate:"required_with=EABKid"` // base64 encoded
HTTPClient *http.Client `json:"-"` // for tests only HTTPClient *http.Client `json:"-"` // for tests only
challengeProvider challenge.Provider challengeProvider challenge.Provider
idx int // 0: main, 1+: extra[i] idx int // 0: main, 1+: extra[i]
} }
)
var ( var (
ErrMissingField = gperr.New("missing field") ErrMissingField = gperr.New("missing field")
@@ -68,13 +66,13 @@ const (
var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`) var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
// Validate implements the serialization.CustomValidator interface. // Validate implements the utils.CustomValidator interface.
func (cfg *Config) Validate() error { func (cfg *Config) Validate() gperr.Error {
seenPaths := make(map[string]int) // path -> provider idx (0 for main, 1+ for extras) seenPaths := make(map[string]int) // path -> provider idx (0 for main, 1+ for extras)
return cfg.validate(seenPaths) return cfg.validate(seenPaths)
} }
func (cfg *ConfigExtra) Validate() error { func (cfg *ConfigExtra) Validate() gperr.Error {
return nil // done by main config's validate return nil // done by main config's validate
} }
@@ -82,7 +80,7 @@ func (cfg *ConfigExtra) AsConfig() *Config {
return (*Config)(cfg) return (*Config)(cfg)
} }
func (cfg *Config) validate(seenPaths map[string]int) error { func (cfg *Config) validate(seenPaths map[string]int) gperr.Error {
if cfg.Provider == "" { if cfg.Provider == "" {
cfg.Provider = ProviderLocal cfg.Provider = ProviderLocal
} }
@@ -159,7 +157,7 @@ func (cfg *Config) validate(seenPaths map[string]int) error {
cfg.Extra[i].AsConfig().idx = i + 1 cfg.Extra[i].AsConfig().idx = i + 1
err := cfg.Extra[i].AsConfig().validate(seenPaths) err := cfg.Extra[i].AsConfig().validate(seenPaths)
if err != nil { if err != nil {
b.AddSubjectf(err, "extra[%d]", i) b.Add(err.Subjectf("extra[%d]", i))
} }
} }
} }
@@ -181,10 +179,10 @@ func (cfg *Config) GetLegoConfig() (*User, *lego.Config, error) {
log.Info().Err(err).Msg("failed to load ACME private key, generating a now one") log.Info().Err(err).Msg("failed to load ACME private key, generating a now one")
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("generate ACME private key: %w", err) return nil, nil, gperr.New("generate ACME private key").With(err)
} }
if err = cfg.SaveACMEKey(privKey); err != nil { if err = cfg.SaveACMEKey(privKey); err != nil {
return nil, nil, fmt.Errorf("save ACME private key: %w", err) return nil, nil, gperr.New("save ACME private key").With(err)
} }
} }
} }
@@ -208,7 +206,7 @@ func (cfg *Config) GetLegoConfig() (*User, *lego.Config, error) {
if len(cfg.CACerts) > 0 { if len(cfg.CACerts) > 0 {
certPool, err := lego.CreateCertPool(cfg.CACerts, true) certPool, err := lego.CreateCertPool(cfg.CACerts, true)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to create cert pool: %w", err) return nil, nil, gperr.New("failed to create cert pool").With(err)
} }
legoCfg.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = certPool legoCfg.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = certPool
} }

View File

@@ -22,7 +22,6 @@ import (
"github.com/go-acme/lego/v4/registration" "github.com/go-acme/lego/v4/registration"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
autocert "github.com/yusing/godoxy/internal/autocert/types"
"github.com/yusing/godoxy/internal/common" "github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/notif" "github.com/yusing/godoxy/internal/notif"
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
@@ -57,6 +56,15 @@ type (
CertExpiries map[string]time.Time CertExpiries map[string]time.Time
CertInfo struct {
Subject string `json:"subject"`
Issuer string `json:"issuer"`
NotBefore int64 `json:"not_before"`
NotAfter int64 `json:"not_after"`
DNSNames []string `json:"dns_names"`
EmailAddresses []string `json:"email_addresses"`
} // @name CertInfo
RenewMode uint8 RenewMode uint8
) )
@@ -74,6 +82,9 @@ const (
renewModeIfNeeded renewModeIfNeeded
) )
// could be nil
var ActiveProvider atomic.Pointer[Provider]
func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) (*Provider, error) { func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) (*Provider, error) {
p := &Provider{ p := &Provider{
cfg: cfg, cfg: cfg,
@@ -108,14 +119,14 @@ func (p *Provider) GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
return p.tlsCert, nil return p.tlsCert, nil
} }
func (p *Provider) GetCertInfos() ([]autocert.CertInfo, error) { func (p *Provider) GetCertInfos() ([]CertInfo, error) {
allProviders := p.allProviders() allProviders := p.allProviders()
certInfos := make([]autocert.CertInfo, 0, len(allProviders)) certInfos := make([]CertInfo, 0, len(allProviders))
for _, provider := range allProviders { for _, provider := range allProviders {
if provider.tlsCert == nil { if provider.tlsCert == nil {
continue continue
} }
certInfos = append(certInfos, autocert.CertInfo{ certInfos = append(certInfos, CertInfo{
Subject: provider.tlsCert.Leaf.Subject.CommonName, Subject: provider.tlsCert.Leaf.Subject.CommonName,
Issuer: provider.tlsCert.Leaf.Issuer.CommonName, Issuer: provider.tlsCert.Leaf.Issuer.CommonName,
NotBefore: provider.tlsCert.Leaf.NotBefore.Unix(), NotBefore: provider.tlsCert.Leaf.NotBefore.Unix(),
@@ -139,7 +150,7 @@ func (p *Provider) GetName() string {
} }
func (p *Provider) fmtError(err error) error { func (p *Provider) fmtError(err error) error {
return gperr.PrependSubject(err, "provider: "+p.GetName()) return gperr.PrependSubject(fmt.Sprintf("provider: %s", p.GetName()), err)
} }
func (p *Provider) GetCertPath() string { func (p *Provider) GetCertPath() string {
@@ -205,15 +216,14 @@ func (p *Provider) ObtainCertIfNotExistsAll() error {
for _, provider := range p.allProviders() { for _, provider := range p.allProviders() {
errs.Go(func() error { errs.Go(func() error {
if err := provider.obtainCertIfNotExists(); err != nil { if err := provider.obtainCertIfNotExists(); err != nil {
return gperr.PrependSubject(err, provider.GetName()) return fmt.Errorf("failed to obtain cert for %s: %w", provider.GetName(), err)
} }
return nil return nil
}) })
} }
err := errs.Wait().Error()
p.rebuildSNIMatcher() p.rebuildSNIMatcher()
return err return errs.Wait().Error()
} }
// obtainCertIfNotExists obtains a new certificate for this provider if it does not exist. // obtainCertIfNotExists obtains a new certificate for this provider if it does not exist.
@@ -246,15 +256,12 @@ func (p *Provider) ObtainCertAll() error {
for _, provider := range p.allProviders() { for _, provider := range p.allProviders() {
errs.Go(func() error { errs.Go(func() error {
if err := provider.obtainCertIfNotExists(); err != nil { if err := provider.obtainCertIfNotExists(); err != nil {
return gperr.PrependSubject(err, provider.GetName()) return fmt.Errorf("failed to obtain cert for %s: %w", provider.GetName(), err)
} }
return nil return nil
}) })
} }
return errs.Wait().Error()
err := errs.Wait().Error()
p.rebuildSNIMatcher()
return err
} }
// ObtainCert renews existing certificate or obtains a new certificate for this provider. // ObtainCert renews existing certificate or obtains a new certificate for this provider.
@@ -464,10 +471,10 @@ func (p *Provider) scheduleRenewal(parent task.Parent) {
renewed, err := p.renew(renewMode) renewed, err := p.renew(renewMode)
if err != nil { if err != nil {
log.Warn().Err(p.fmtError(err)).Msg("autocert: cert renew failed") gperr.LogWarn("autocert: cert renew failed", p.fmtError(err))
notif.Notify(&notif.LogMessage{ notif.Notify(&notif.LogMessage{
Level: zerolog.ErrorLevel, Level: zerolog.ErrorLevel,
Title: "SSL certificate renewal failed for " + p.GetName(), Title: fmt.Sprintf("SSL certificate renewal failed for %s", p.GetName()),
Body: notif.MessageBody(err.Error()), Body: notif.MessageBody(err.Error()),
}) })
return return
@@ -477,13 +484,13 @@ func (p *Provider) scheduleRenewal(parent task.Parent) {
notif.Notify(&notif.LogMessage{ notif.Notify(&notif.LogMessage{
Level: zerolog.InfoLevel, Level: zerolog.InfoLevel,
Title: "SSL certificate renewed for " + p.GetName(), Title: fmt.Sprintf("SSL certificate renewed for %s", p.GetName()),
Body: notif.ListBody(p.cfg.Domains), Body: notif.ListBody(p.cfg.Domains),
}) })
// Reset on success // Reset on success
if err := p.ClearLastFailure(); err != nil { if err := p.ClearLastFailure(); err != nil {
log.Warn().Err(p.fmtError(err)).Msg("autocert: failed to clear last failure") gperr.LogWarn("autocert: failed to clear last failure", p.fmtError(err))
} }
timer.Reset(time.Until(p.ShouldRenewOn())) timer.Reset(time.Until(p.ShouldRenewOn()))
} }

View File

@@ -68,7 +68,7 @@ func TestMultipleCertificatesLifecycle(t *testing.T) {
require.Equal(t, "custom", cfg.Extra[1].Provider) require.Equal(t, "custom", cfg.Extra[1].Provider)
/* track cert requests for all configs */ /* track cert requests for all configs */
os.MkdirAll("certs", 0o755) os.MkdirAll("certs", 0755)
defer os.RemoveAll("certs") defer os.RemoveAll("certs")
err = provider.ObtainCertIfNotExistsAll() err = provider.ObtainCertIfNotExistsAll()

View File

@@ -3,10 +3,11 @@ package autocert
import ( import (
"github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge"
"github.com/yusing/godoxy/internal/serialization" "github.com/yusing/godoxy/internal/serialization"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings" strutils "github.com/yusing/goutils/strings"
) )
type Generator func(map[string]strutils.Redacted) (challenge.Provider, error) type Generator func(map[string]strutils.Redacted) (challenge.Provider, gperr.Error)
var Providers = make(map[string]Generator) var Providers = make(map[string]Generator)
@@ -14,7 +15,7 @@ func DNSProvider[CT any, PT challenge.Provider](
defaultCfg func() *CT, defaultCfg func() *CT,
newProvider func(*CT) (PT, error), newProvider func(*CT) (PT, error),
) Generator { ) Generator {
return func(opt map[string]strutils.Redacted) (challenge.Provider, error) { return func(opt map[string]strutils.Redacted) (challenge.Provider, gperr.Error) {
cfg := defaultCfg() cfg := defaultCfg()
if len(opt) > 0 { if len(opt) > 0 {
err := serialization.MapUnmarshalValidate(serialization.ToSerializedObject(opt), &cfg) err := serialization.MapUnmarshalValidate(serialization.ToSerializedObject(opt), &cfg)
@@ -23,6 +24,6 @@ func DNSProvider[CT any, PT challenge.Provider](
} }
} }
p, pErr := newProvider(cfg) p, pErr := newProvider(cfg)
return p, pErr return p, gperr.Wrap(pErr)
} }
} }

View File

@@ -4,7 +4,7 @@ import (
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
) )
func (p *Provider) setupExtraProviders() error { func (p *Provider) setupExtraProviders() gperr.Error {
p.sniMatcher = sniMatcher{} p.sniMatcher = sniMatcher{}
if len(p.cfg.Extra) == 0 { if len(p.cfg.Extra) == 0 {
return nil return nil

View File

@@ -1,10 +0,0 @@
package autocert
type CertInfo struct {
Subject string `json:"subject"`
Issuer string `json:"issuer"`
NotBefore int64 `json:"not_before"`
NotAfter int64 `json:"not_after"`
DNSNames []string `json:"dns_names"`
EmailAddresses []string `json:"email_addresses"`
} // @name CertInfo

View File

@@ -1,16 +0,0 @@
package autocert
import "context"
type ContextKey struct{}
func SetCtx(ctx interface{ SetValue(key, value any) }, p Provider) {
ctx.SetValue(ContextKey{}, p)
}
func FromCtx(ctx context.Context) Provider {
if provider, ok := ctx.Value(ContextKey{}).(Provider); ok {
return provider
}
return nil
}

View File

@@ -1,17 +1,14 @@
package autocert package autocert
import ( import (
"context"
"crypto/tls" "crypto/tls"
"github.com/yusing/goutils/task" "github.com/yusing/goutils/task"
) )
type Provider interface { type Provider interface {
GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error) Setup() error
GetCertInfos() ([]CertInfo, error) GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error)
ScheduleRenewalAll(parent task.Parent) ScheduleRenewalAll(task.Parent)
ObtainCertAll() error ObtainCertAll() error
ForceExpiryAll() bool
WaitRenewalDone(ctx context.Context) bool
} }

View File

@@ -1,4 +1,4 @@
# internal/config # Configuration Management
Centralized YAML configuration management with thread-safe state access and provider initialization. Centralized YAML configuration management with thread-safe state access and provider initialization.
@@ -54,7 +54,7 @@ type State interface {
Task() *task.Task Task() *task.Task
Context() context.Context Context() context.Context
Value() *Config Value() *Config
Entrypoint() entrypoint.Entrypoint EntrypointHandler() http.Handler
ShortLinkMatcher() config.ShortLinkMatcher ShortLinkMatcher() config.ShortLinkMatcher
AutoCertProvider() server.CertProvider AutoCertProvider() server.CertProvider
LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool) LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool)
@@ -62,12 +62,6 @@ type State interface {
IterProviders() iter.Seq2[string, types.RouteProvider] IterProviders() iter.Seq2[string, types.RouteProvider]
StartProviders() error StartProviders() error
NumProviders() int NumProviders() int
// Lifecycle management
StartAPIServers()
StartMetrics()
FlushTmpLog()
} }
``` ```
@@ -220,15 +214,12 @@ Configuration supports hot-reloading via editing `config/config.yml`.
- `internal/acl` - Access control configuration - `internal/acl` - Access control configuration
- `internal/autocert` - SSL certificate management - `internal/autocert` - SSL certificate management
- `internal/entrypoint` - HTTP entrypoint setup (now via interface) - `internal/entrypoint` - HTTP entrypoint setup
- `internal/route/provider` - Route providers (Docker, file, agent) - `internal/route/provider` - Route providers (Docker, file, agent)
- `internal/maxmind` - GeoIP configuration - `internal/maxmind` - GeoIP configuration
- `internal/notif` - Notification providers - `internal/notif` - Notification providers
- `internal/proxmox` - LXC container management - `internal/proxmox` - LXC container management
- `internal/homepage/types` - Dashboard configuration - `internal/homepage/types` - Dashboard configuration
- `internal/api` - REST API servers
- `internal/metrics/systeminfo` - System metrics polling
- `internal/metrics/uptime` - Uptime tracking
- `github.com/yusing/goutils/task` - Object lifecycle management - `github.com/yusing/goutils/task` - Object lifecycle management
### External dependencies ### External dependencies
@@ -321,8 +312,5 @@ for name, provider := range config.GetState().IterProviders() {
```go ```go
state := config.GetState() state := config.GetState()
// Get entrypoint interface for route management http.Handle("/", state.EntrypointHandler())
ep := state.Entrypoint()
// Add routes directly to entrypoint
ep.AddRoute(route)
``` ```

View File

@@ -7,15 +7,14 @@ import (
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common" "github.com/yusing/godoxy/internal/common"
config "github.com/yusing/godoxy/internal/config/types" config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/notif" "github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/watcher" "github.com/yusing/godoxy/internal/watcher"
watcherEvents "github.com/yusing/godoxy/internal/watcher/events" "github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/eventqueue" "github.com/yusing/goutils/server"
"github.com/yusing/goutils/events"
"github.com/yusing/goutils/strings/ansi" "github.com/yusing/goutils/strings/ansi"
"github.com/yusing/goutils/task" "github.com/yusing/goutils/task"
) )
@@ -27,29 +26,29 @@ var (
const configEventFlushInterval = 500 * time.Millisecond const configEventFlushInterval = 500 * time.Millisecond
var ( const (
errCfgRenameWarn = errors.New("config file renamed, not reloading; Make sure you rename it back before next time you start") cfgRenameWarn = `Config file renamed, not reloading.
errCfgDeleteWarn = errors.New(`config file deleted, not reloading; You may run "ls-config" to show or dump the current config`) Make sure you rename it back before next time you start.`
cfgDeleteWarn = `Config file deleted, not reloading.
You may run "ls-config" to show or dump the current config.`
) )
func logNotifyError(action string, err error) { func logNotifyError(action string, err error) {
log.Error().Err(err).Msg("config " + action + " error") gperr.LogError("config "+action+" error", err)
notif.Notify(&notif.LogMessage{ notif.Notify(&notif.LogMessage{
Level: zerolog.ErrorLevel, Level: zerolog.ErrorLevel,
Title: fmt.Sprintf("Config %s error", action), Title: fmt.Sprintf("Config %s error", action),
Body: notif.ErrorBody(err), Body: notif.ErrorBody(err),
}) })
events.Global.Add(events.NewEvent(events.LevelError, "config", action, err))
} }
func logNotifyWarn(action string, err error) { func logNotifyWarn(action string, err error) {
log.Warn().Err(err).Msg("config " + action + " warning") gperr.LogWarn("config "+action+" error", err)
notif.Notify(&notif.LogMessage{ notif.Notify(&notif.LogMessage{
Level: zerolog.WarnLevel, Level: zerolog.WarnLevel,
Title: fmt.Sprintf("Config %s warning", action), Title: fmt.Sprintf("Config %s warning", action),
Body: notif.ErrorBody(err), Body: notif.ErrorBody(err),
}) })
events.Global.Add(events.NewEvent(events.LevelWarn, "config", action, err))
} }
func Load() error { func Load() error {
@@ -61,29 +60,20 @@ func Load() error {
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName) cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
initErr := state.InitFromFile(common.ConfigPath)
if initErr != nil {
// if error is critical, notify and return it without starting providers
if criticalErr, ok := errors.AsType[CriticalError](initErr); ok {
logNotifyError("init", criticalErr.err)
return criticalErr
}
}
// disable pool logging temporary since we already have pretty logging // disable pool logging temporary since we already have pretty logging
state.Entrypoint().DisablePoolsLog(true) routes.HTTP.DisableLog(true)
routes.Stream.DisableLog(true)
defer func() { defer func() {
state.Entrypoint().DisablePoolsLog(false) routes.HTTP.DisableLog(false)
routes.Stream.DisableLog(false)
}() }()
initErr := state.InitFromFile(common.ConfigPath)
err := errors.Join(initErr, state.StartProviders()) err := errors.Join(initErr, state.StartProviders())
if err != nil { if err != nil {
logNotifyError("init", err) logNotifyError("init", err)
} }
state.StartAPIServers()
state.StartMetrics()
SetState(state) SetState(state)
// flush temporary log // flush temporary log
@@ -91,9 +81,7 @@ func Load() error {
return nil return nil
} }
func Reload() error { func Reload() gperr.Error {
events.Global.Add(events.NewEvent(events.LevelInfo, "config", "reload", nil))
// avoid race between config change and API reload request // avoid race between config change and API reload request
reloadMu.Lock() reloadMu.Lock()
defer reloadMu.Unlock() defer reloadMu.Unlock()
@@ -120,35 +108,32 @@ func Reload() error {
logNotifyError("start providers", err) logNotifyError("start providers", err)
return nil // continue return nil // continue
} }
StartProxyServers()
newState.StartAPIServers()
newState.StartMetrics()
return nil return nil
} }
func WatchChanges() { func WatchChanges() {
opts := eventqueue.Options[watcherEvents.Event]{ t := task.RootTask("config_watcher", true)
FlushInterval: configEventFlushInterval, eventQueue := events.NewEventQueue(
OnFlush: OnConfigChange, t,
OnError: func(err error) { configEventFlushInterval,
OnConfigChange,
func(err gperr.Error) {
logNotifyError("reload", err) logNotifyError("reload", err)
}, },
Debug: common.IsDebug, )
}
t := task.RootTask("config_watcher", true)
eventQueue := eventqueue.New(t, opts)
eventQueue.Start(cfgWatcher.Events(t.Context())) eventQueue.Start(cfgWatcher.Events(t.Context()))
} }
func OnConfigChange(ev []watcherEvents.Event) { func OnConfigChange(ev []events.Event) {
// no matter how many events during the interval // no matter how many events during the interval
// just reload once and check the last event // just reload once and check the last event
switch ev[len(ev)-1].Action { switch ev[len(ev)-1].Action {
case watcherEvents.ActionFileRenamed: case events.ActionFileRenamed:
logNotifyWarn("rename", errCfgRenameWarn) logNotifyWarn("rename", errors.New(cfgRenameWarn))
return return
case watcherEvents.ActionFileDeleted: case events.ActionFileDeleted:
logNotifyWarn("delete", errCfgDeleteWarn) logNotifyWarn("delete", errors.New(cfgDeleteWarn))
return return
} }
@@ -157,3 +142,16 @@ func OnConfigChange(ev []watcherEvents.Event) {
panic(err) panic(err)
} }
} }
func StartProxyServers() {
cfg := GetState()
server.StartServer(cfg.Task(), server.Options{
Name: "proxy",
CertProvider: cfg.AutoCertProvider(),
HTTPAddr: common.ProxyHTTPAddr,
HTTPSAddr: common.ProxyHTTPSAddr,
Handler: cfg.EntrypointHandler(),
ACL: cfg.Value().ACL,
SupportProxyProtocol: cfg.Value().Entrypoint.SupportProxyProtocol,
})
}

View File

@@ -1,4 +1,4 @@
# internal/config/query # Configuration Query
Read-only access to the active configuration state, including route providers and system statistics. Read-only access to the active configuration state, including route providers and system statistics.
@@ -149,7 +149,7 @@ No metrics are currently exposed.
## Performance Characteristics ## Performance Characteristics
- O(n) where n is number of providers for provider queries - O(n) where n is number of providers for provider queries
- O(n \* m) where m is routes per provider for route search - O(n * m) where m is routes per provider for route search
- O(n) for statistics aggregation - O(n) for statistics aggregation
- No locking required (uses atomic load) - No locking required (uses atomic load)

View File

@@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"iter" "iter"
"net/http"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@@ -17,21 +18,14 @@ import (
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
"github.com/puzpuzpuz/xsync/v4" "github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/yusing/godoxy/internal/acl"
acl "github.com/yusing/godoxy/internal/acl/types"
"github.com/yusing/godoxy/internal/agentpool" "github.com/yusing/godoxy/internal/agentpool"
"github.com/yusing/godoxy/internal/api"
"github.com/yusing/godoxy/internal/autocert" "github.com/yusing/godoxy/internal/autocert"
autocertctx "github.com/yusing/godoxy/internal/autocert/types"
"github.com/yusing/godoxy/internal/common"
config "github.com/yusing/godoxy/internal/config/types" config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/entrypoint" "github.com/yusing/godoxy/internal/entrypoint"
entrypointctx "github.com/yusing/godoxy/internal/entrypoint/types"
homepage "github.com/yusing/godoxy/internal/homepage/types" homepage "github.com/yusing/godoxy/internal/homepage/types"
"github.com/yusing/godoxy/internal/logging" "github.com/yusing/godoxy/internal/logging"
"github.com/yusing/godoxy/internal/maxmind" "github.com/yusing/godoxy/internal/maxmind"
"github.com/yusing/godoxy/internal/metrics/systeminfo"
"github.com/yusing/godoxy/internal/metrics/uptime"
"github.com/yusing/godoxy/internal/notif" "github.com/yusing/godoxy/internal/notif"
route "github.com/yusing/godoxy/internal/route/provider" route "github.com/yusing/godoxy/internal/route/provider"
"github.com/yusing/godoxy/internal/serialization" "github.com/yusing/godoxy/internal/serialization"
@@ -46,7 +40,7 @@ type state struct {
providers *xsync.Map[string, types.RouteProvider] providers *xsync.Map[string, types.RouteProvider]
autocertProvider *autocert.Provider autocertProvider *autocert.Provider
entrypoint *entrypoint.Entrypoint entrypoint entrypoint.Entrypoint
task *task.Task task *task.Task
@@ -56,25 +50,14 @@ type state struct {
tmpLog zerolog.Logger tmpLog zerolog.Logger
} }
type CriticalError struct {
err error
}
func (e CriticalError) Error() string {
return e.err.Error()
}
func (e CriticalError) Unwrap() error {
return e.err
}
func NewState() config.State { func NewState() config.State {
tmpLogBuf := bytes.NewBuffer(make([]byte, 0, 4096)) tmpLogBuf := bytes.NewBuffer(make([]byte, 0, 4096))
return &state{ return &state{
providers: xsync.NewMap[string, types.RouteProvider](), providers: xsync.NewMap[string, types.RouteProvider](),
task: task.RootTask("config", false), entrypoint: entrypoint.NewEntrypoint(),
tmpLogBuf: tmpLogBuf, task: task.RootTask("config", false),
tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf), tmpLogBuf: tmpLogBuf,
tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf),
} }
} }
@@ -90,7 +73,13 @@ func SetState(state config.State) {
cfg := state.Value() cfg := state.Value()
config.ActiveState.Store(state) config.ActiveState.Store(state)
entrypoint.ActiveConfig.Store(&cfg.Entrypoint)
homepage.ActiveConfig.Store(&cfg.Homepage) homepage.ActiveConfig.Store(&cfg.Homepage)
if autocertProvider := state.AutoCertProvider(); autocertProvider != nil {
autocert.ActiveProvider.Store(autocertProvider.(*autocert.Provider))
} else {
autocert.ActiveProvider.Store(nil)
}
} }
func HasState() bool { func HasState() bool {
@@ -107,7 +96,7 @@ func (state *state) InitFromFile(filename string) error {
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
state.Config = config.DefaultConfig() state.Config = config.DefaultConfig()
} else { } else {
return CriticalError{err} return err
} }
} }
return state.Init(data) return state.Init(data)
@@ -116,7 +105,7 @@ func (state *state) InitFromFile(filename string) error {
func (state *state) Init(data []byte) error { func (state *state) Init(data []byte) error {
err := serialization.UnmarshalValidate(data, &state.Config, yaml.Unmarshal) err := serialization.UnmarshalValidate(data, &state.Config, yaml.Unmarshal)
if err != nil { if err != nil {
return CriticalError{err} return err
} }
g := gperr.NewGroup("config load error") g := gperr.NewGroup("config load error")
@@ -128,9 +117,7 @@ func (state *state) Init(data []byte) error {
// these won't benefit from running on goroutines // these won't benefit from running on goroutines
errs.Add(state.initNotification()) errs.Add(state.initNotification())
errs.Add(state.initACL()) errs.Add(state.initACL())
if err := state.initEntrypoint(); err != nil { errs.Add(state.initEntrypoint())
errs.Add(CriticalError{err})
}
errs.Add(state.loadRouteProviders()) errs.Add(state.loadRouteProviders())
return errs.Error() return errs.Error()
} }
@@ -147,8 +134,8 @@ func (state *state) Value() *config.Config {
return &state.Config return &state.Config
} }
func (state *state) Entrypoint() entrypointctx.Entrypoint { func (state *state) EntrypointHandler() http.Handler {
return state.entrypoint return &state.entrypoint
} }
func (state *state) ShortLinkMatcher() config.ShortLinkMatcher { func (state *state) ShortLinkMatcher() config.ShortLinkMatcher {
@@ -199,39 +186,10 @@ func (state *state) NumProviders() int {
} }
func (state *state) FlushTmpLog() { func (state *state) FlushTmpLog() {
_, _ = state.tmpLogBuf.WriteTo(os.Stdout) state.tmpLogBuf.WriteTo(os.Stdout)
state.tmpLogBuf.Reset() state.tmpLogBuf.Reset()
} }
func (state *state) StartAPIServers() {
// API Handler needs to start after auth is initialized.
_, err := server.StartServer(state.task.Subtask("api_server", false), server.Options{
Name: "api",
HTTPAddr: common.APIHTTPAddr,
Handler: api.NewHandler(true),
})
if err != nil {
log.Err(err).Msg("failed to start API server")
}
// Local API Handler is used for unauthenticated access.
if common.LocalAPIHTTPAddr != "" {
_, err := server.StartServer(state.task.Subtask("local_api_server", false), server.Options{
Name: "local_api",
HTTPAddr: common.LocalAPIHTTPAddr,
Handler: api.NewHandler(false),
})
if err != nil {
log.Err(err).Msg("failed to start local API server")
}
}
}
func (state *state) StartMetrics() {
systeminfo.Poller.Start(state.task)
uptime.Poller.Start(state.task)
}
// initACL initializes the ACL. // initACL initializes the ACL.
func (state *state) initACL() error { func (state *state) initACL() error {
if !state.ACL.Valid() { if !state.ACL.Valid() {
@@ -241,7 +199,7 @@ func (state *state) initACL() error {
if err != nil { if err != nil {
return err return err
} }
acl.SetCtx(state.task, state.ACL) state.task.SetValue(acl.ContextKey{}, state.ACL)
return nil return nil
} }
@@ -249,7 +207,6 @@ func (state *state) initEntrypoint() error {
epCfg := state.Config.Entrypoint epCfg := state.Config.Entrypoint
matchDomains := state.MatchDomains matchDomains := state.MatchDomains
state.entrypoint = entrypoint.NewEntrypoint(state.task, &epCfg)
state.entrypoint.SetFindRouteDomains(matchDomains) state.entrypoint.SetFindRouteDomains(matchDomains)
state.entrypoint.SetNotFoundRules(epCfg.Rules.NotFound) state.entrypoint.SetNotFoundRules(epCfg.Rules.NotFound)
@@ -263,8 +220,6 @@ func (state *state) initEntrypoint() error {
} }
} }
entrypointctx.SetCtx(state.task, state.entrypoint)
errs := gperr.NewBuilder("entrypoint error") errs := gperr.NewBuilder("entrypoint error")
errs.Add(state.entrypoint.SetMiddlewares(epCfg.Middlewares)) errs.Add(state.entrypoint.SetMiddlewares(epCfg.Middlewares))
errs.Add(state.entrypoint.SetAccessLogger(state.task, epCfg.AccessLog)) errs.Add(state.entrypoint.SetAccessLogger(state.task, epCfg.AccessLog))
@@ -341,7 +296,6 @@ func (state *state) initAutoCert() error {
p.PrintCertExpiriesAll() p.PrintCertExpiriesAll()
state.autocertProvider = p state.autocertProvider = p
autocertctx.SetCtx(state.task, p)
return nil return nil
} }
@@ -355,7 +309,7 @@ func (state *state) initProxmox() error {
for _, cfg := range proxmoxCfg { for _, cfg := range proxmoxCfg {
errs.Go(func() error { errs.Go(func() error {
if err := cfg.Init(state.task.Context()); err != nil { if err := cfg.Init(state.task.Context()); err != nil {
return gperr.PrependSubject(err, cfg.URL) return err.Subject(cfg.URL)
} }
return nil return nil
}) })
@@ -379,7 +333,7 @@ func (state *state) loadRouteProviders() error {
for _, a := range providers.Agents { for _, a := range providers.Agents {
agentErrs.Go(func() error { agentErrs.Go(func() error {
if err := a.Init(state.task.Context()); err != nil { if err := a.Init(state.task.Context()); err != nil {
return gperr.PrependSubject(err, a.String()) return gperr.PrependSubject(a.String(), err)
} }
agentpool.Add(a) agentpool.Add(a)
return nil return nil
@@ -397,7 +351,7 @@ func (state *state) loadRouteProviders() error {
for _, filename := range providers.Files { for _, filename := range providers.Files {
p, err := route.NewFileProvider(filename) p, err := route.NewFileProvider(filename)
if err != nil { if err != nil {
errs.Add(gperr.PrependSubject(err, filename)) errs.Add(gperr.PrependSubject(filename, err))
return err return err
} }
registerProvider(p) registerProvider(p)
@@ -422,7 +376,7 @@ func (state *state) loadRouteProviders() error {
for _, p := range state.providers.Range { for _, p := range state.providers.Range {
loadErrs.Go(func() error { loadErrs.Go(func() error {
if err := p.LoadRoutes(); err != nil { if err := p.LoadRoutes(); err != nil {
return gperr.PrependSubject(err, p.String()) return err.Subject(p.String())
} }
resultsMu.Lock() resultsMu.Lock()
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes()) results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())

View File

@@ -8,13 +8,14 @@ import (
"github.com/yusing/godoxy/agent/pkg/agent" "github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/acl" "github.com/yusing/godoxy/internal/acl"
"github.com/yusing/godoxy/internal/autocert" "github.com/yusing/godoxy/internal/autocert"
"github.com/yusing/godoxy/internal/entrypoint" entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
homepage "github.com/yusing/godoxy/internal/homepage/types" homepage "github.com/yusing/godoxy/internal/homepage/types"
maxmind "github.com/yusing/godoxy/internal/maxmind/types" maxmind "github.com/yusing/godoxy/internal/maxmind/types"
"github.com/yusing/godoxy/internal/notif" "github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/proxmox" "github.com/yusing/godoxy/internal/proxmox"
"github.com/yusing/godoxy/internal/serialization" "github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/types" "github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
) )
type ( type (
@@ -41,7 +42,7 @@ type (
} }
) )
func Validate(data []byte) error { func Validate(data []byte) gperr.Error {
var model Config var model Config
return serialization.UnmarshalValidate(data, &model, yaml.Unmarshal) return serialization.UnmarshalValidate(data, &model, yaml.Unmarshal)
} }

View File

@@ -6,7 +6,6 @@ import (
"iter" "iter"
"net/http" "net/http"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/types" "github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/server" "github.com/yusing/goutils/server"
"github.com/yusing/goutils/synk" "github.com/yusing/goutils/synk"
@@ -22,7 +21,7 @@ type State interface {
Value() *Config Value() *Config
Entrypoint() entrypoint.Entrypoint EntrypointHandler() http.Handler
ShortLinkMatcher() ShortLinkMatcher ShortLinkMatcher() ShortLinkMatcher
AutoCertProvider() server.CertProvider AutoCertProvider() server.CertProvider
@@ -33,9 +32,6 @@ type State interface {
StartProviders() error StartProviders() error
FlushTmpLog() FlushTmpLog()
StartAPIServers()
StartMetrics()
} }
type ShortLinkMatcher interface { type ShortLinkMatcher interface {

View File

@@ -1,4 +1,4 @@
# internal/dnsproviders # DNS Providers
DNS provider integrations for Let's Encrypt certificate management via the lego library. DNS provider integrations for Let's Encrypt certificate management via the lego library.

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