This commit is contained in:
yusing
2026-02-16 08:59:01 +08:00
parent 15b9635ee1
commit e4e6f6b3e8
242 changed files with 3953 additions and 3502 deletions

16
.gitignore vendored
View File

@@ -14,34 +14,30 @@ 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.*

View File

@@ -47,6 +47,7 @@ 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)?$
@@ -55,21 +56,15 @@ 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
@@ -106,8 +101,7 @@ linters:
checks: checks:
- all - all
- -SA1019 - -SA1019
dot-import-whitelist: - -QF1008 # keep embedded field selector for clarity
- github.com/yusing/godoxy/internal/utils/testing
tagalign: tagalign:
align: false align: false
sort: true sort: true
@@ -135,9 +129,8 @@ 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
@@ -146,6 +139,7 @@ formatters:
exclusions: exclusions:
generated: lax generated: lax
paths: paths:
- third_party$
- builtin$
- examples$ - examples$
- internal/api/v1/.+
run:
tests: false

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,11 @@
# Stage 1: deps # Stage 1: deps
FROM golang:1.25.6-alpine AS deps FROM golang:1.26.0-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 RUN apk add --no-cache tzdata make libcap-setcap libgcc libstdc++
ENV GOPATH=/root/go ENV GOPATH=/root/go
ENV GOCACHE=/root/.cache/go-build ENV GOCACHE=/root/.cache/go-build
@@ -17,6 +18,10 @@ 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) export VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null)
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
@@ -117,12 +117,27 @@ mod-tidy:
cd ${PWD}/$$path && go mod tidy; \ cd ${PWD}/$$path && go mod tidy; \
done done
build: minify-js:
@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: run: minify-js
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:
@@ -132,16 +147,12 @@ 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:
@if [ -z "$(TARGET)" ]; then \ @TARGETS="$(TARGET)"; \
docker compose -f dev.compose.yml up -d --force-recreate godoxy traefik caddy nginx; \ if [ -z "$$TARGETS" ]; then TARGETS="godoxy traefik caddy nginx"; fi; \
else \ trap 'docker compose -f dev.compose.yml down $$TARGETS' EXIT; \
docker compose -f dev.compose.yml up -d --force-recreate $(TARGET); \ docker compose -f dev.compose.yml up -d --force-recreate $$TARGETS; \
fi sleep 1; \
sleep 1 ./scripts/benchmark.sh
@./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 &&\
@@ -175,8 +186,7 @@ gen-swagger-markdown: gen-swagger
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}/lib -n api.ts -p internal/api/v1/docs/swagger.json --responses -o ${WEBUI_DIR}/src/lib -n api.ts -p internal/api/v1/docs/swagger.json
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
.PHONY: update-wiki .PHONY: update-wiki
update-wiki: update-wiki:

View File

@@ -19,7 +19,6 @@ 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"
@@ -72,7 +71,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 {
gperr.LogFatal("failed to listen on port", err) log.Fatal().Err(err).Msg("failed to listen on port")
} }
caCertPool := x509.NewCertPool() caCertPool := x509.NewCertPool()
@@ -148,7 +147,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 {
gperr.LogFatal("failed to listen on port", err) log.Fatal().Err(err).Msg("failed to listen on port")
} }
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{
@@ -158,10 +157,15 @@ Tips:
}, },
ErrorLog: stdlog.New(&errLog, "", 0), ErrorLog: stdlog.New(&errLog, "", 0),
} }
srv.Serve(l) go func() {
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() systeminfo.Poller.Start(t)
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.25.6 go 1.26.0
replace ( replace (
github.com/shirou/gopsutil/v4 => ../internal/gopsutil github.com/shirou/gopsutil/v4 => ../internal/gopsutil
@@ -19,11 +19,11 @@ exclude github.com/yusing/godoxy/internal/utils v0.0.0-20250927032450-e2aeef3a86
require ( require (
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.0.10 github.com/pion/dtls/v3 v3.1.2
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.25.2 github.com/yusing/godoxy v0.26.0
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
) )
@@ -40,13 +40,13 @@ 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.0+incompatible // indirect github.com/docker/cli v29.2.1+incompatible // indirect
github.com/docker/docker v28.5.2+incompatible // indirect github.com/docker/docker v28.5.2+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.12 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // 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
@@ -58,7 +58,7 @@ 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.3 // indirect github.com/klauspost/compress v1.18.4 // 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-20251013123823-9fd1530e3ec3 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // 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.25.12 // indirect github.com/shirou/gopsutil/v4 v4.26.1 // 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,21 +90,21 @@ 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.1.16 // indirect github.com/yusing/gointernals v0.2.0 // indirect
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468 // indirect github.com/yusing/goutils/http/reverseproxy v0.0.0-20260215081811-494ab85a33ae // indirect
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468 // indirect github.com/yusing/goutils/http/websocket v0.0.0-20260215081811-494ab85a33ae // 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.64.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect
golang.org/x/arch v0.23.0 // indirect golang.org/x/arch v0.24.0 // indirect
golang.org/x/crypto v0.47.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.49.0 // indirect golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // 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

@@ -41,8 +41,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.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM= github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
@@ -55,8 +55,8 @@ 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.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.13/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=
@@ -99,16 +99,16 @@ 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.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA= github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA= github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
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.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.4/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=
@@ -159,16 +159,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.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg= github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
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.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U= github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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=
@@ -189,10 +189,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.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI= github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI= github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc= github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k=
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=
@@ -225,44 +225,44 @@ 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.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34= github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtSVvg=
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34= github.com/yusing/gointernals v0.2.0/go.mod h1:xGzNbPGMm5Z8kG0t4JYISMscw+gMQlgghkLxlgRZv5Y=
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.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
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.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.35.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=
@@ -271,20 +271,20 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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

@@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/json"
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
@@ -15,7 +16,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/bytedance/sonic"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent/common" "github.com/yusing/godoxy/agent/pkg/agent/common"
@@ -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 {
gperr.LogWarn("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work", err, &cfg.l) cfg.l.Warn().Err(err).Msg("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work")
} }
if serverVersion.IsNewerThanMajor(cfg.Version) { if serverVersion.IsNewerThanMajor(cfg.Version) {
@@ -366,7 +366,7 @@ func (cfg *AgentConfig) fetchJSON(ctx context.Context, endpoint string, out any)
return resp.StatusCode, nil return resp.StatusCode, nil
} }
err = sonic.Unmarshal(data, out) err = json.Unmarshal(data, out)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@@ -1,4 +1,4 @@
FROM golang:1.25.6-alpine AS builder FROM golang:1.26.0-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.25.6 go 1.26.0

View File

@@ -181,7 +181,6 @@ 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.25.6-alpine AS builder FROM golang:1.26.0-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.25.6 go 1.26.0
require golang.org/x/net v0.49.0 require golang.org/x/net v0.50.0
require golang.org/x/text v0.33.0 // indirect require golang.org/x/text v0.34.0 // indirect

View File

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

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,12 +14,8 @@ 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"
) )
@@ -51,7 +47,6 @@ func main() {
parallel( parallel(
dnsproviders.InitProviders, dnsproviders.InitProviders,
iconlist.InitCache, iconlist.InitCache,
systeminfo.Poller.Start,
middleware.LoadComposeFiles, middleware.LoadComposeFiles,
) )
@@ -66,35 +61,19 @@ func main() {
err := config.Load() err := config.Load()
if err != nil { if err != nil {
gperr.LogWarn("errors in config", err) if criticalErr, ok := errors.AsType[config.CriticalError](err); ok {
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:
- /app/.next/cache # next image caching - /tmp:rw
- /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

78
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/yusing/godoxy module github.com/yusing/godoxy
go 1.25.6 go 1.26.0
replace ( replace (
github.com/coreos/go-oidc/v3 => ./internal/go-oidc github.com/coreos/go-oidc/v3 => ./internal/go-oidc
@@ -26,15 +26,15 @@ require (
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.8.0 // reference the Message struct for json response github.com/gotify/server/v2 v2.9.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.9.2 // proxy protocol support github.com/pires/go-proxyproto v0.11.0 // 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.47.0 // encrypting password with bcrypt golang.org/x/crypto v0.48.0 // encrypting password with bcrypt
golang.org/x/net v0.49.0 // HTTP header utilities golang.org/x/net v0.50.0 // HTTP header utilities
golang.org/x/oauth2 v0.34.0 // oauth2 authentication golang.org/x/oauth2 v0.35.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
) )
@@ -42,28 +42,28 @@ 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 // indirect; fast json parsing github.com/bytedance/sonic v1.15.0 // indirect; fast json parsing
github.com/docker/cli v29.2.0+incompatible // needs docker/cli/cli/connhelper connection helper for docker client github.com/docker/cli v29.2.1+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 github.com/golang-jwt/jwt/v5 v5.3.1
github.com/luthermonson/go-proxmox v0.3.2 github.com/luthermonson/go-proxmox v0.3.2
github.com/oschwald/maxminddb-golang v1.13.1 github.com/oschwald/maxminddb-golang v1.13.1
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.25.12 // system information github.com/shirou/gopsutil/v4 v4.26.1 // 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-20260129101716-0f13004ad6ba github.com/yusing/godoxy/agent v0.0.0-20260216003355-b4a9f44f4ee9
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260129101716-0f13004ad6ba github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260216003355-b4a9f44f4ee9
github.com/yusing/gointernals v0.1.16 github.com/yusing/gointernals v0.2.0
github.com/yusing/goutils v0.7.0 github.com/yusing/goutils v0.7.0
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468 github.com/yusing/goutils/http/reverseproxy v0.0.0-20260215081811-494ab85a33ae
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468 github.com/yusing/goutils/http/websocket v0.0.0-20260215081811-494ab85a33ae
github.com/yusing/goutils/server v0.0.0-20260129081554-24e52ede7468 github.com/yusing/goutils/server v0.0.0-20260215081811-494ab85a33ae
) )
require ( require (
cloud.google.com/go/auth v0.18.1 // indirect cloud.google.com/go/auth v0.18.2 // 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
@@ -85,7 +85,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.12 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // 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
@@ -94,8 +94,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.11 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect github.com/googleapis/gax-go/v2 v2.17.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
@@ -121,25 +121,25 @@ 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.19.0 // indirect github.com/samber/slog-common v0.20.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.64.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
go.opentelemetry.io/otel v1.39.0 // indirect go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.40.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.32.0 // indirect golang.org/x/mod v0.33.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect golang.org/x/tools v0.42.0 // indirect
google.golang.org/api v0.263.0 // indirect google.golang.org/api v0.266.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.78.0 // indirect google.golang.org/grpc v1.79.1 // 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
@@ -165,37 +165,37 @@ 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.1 // indirect github.com/go-resty/resty/v2 v2.17.2 // 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/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/compress v1.18.4 // 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.64.0 // indirect github.com/linode/linodego v1.65.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // 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.107.0 // indirect github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 // indirect github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1 // 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.0.10 // indirect github.com/pion/dtls/v3 v3.1.2 // 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
github.com/pquerna/otp v1.5.0 // indirect github.com/pquerna/otp v1.5.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect github.com/samber/slog-zerolog/v2 v2.9.1 // indirect
github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/objx v0.5.3 // 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
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vultr/govultr/v3 v3.26.1 // indirect github.com/vultr/govultr/v3 v3.27.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect
golang.org/x/arch v0.23.0 // indirect golang.org/x/arch v0.24.0 // indirect
) )

144
go.sum
View File

@@ -1,5 +1,5 @@
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
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=
@@ -80,8 +80,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.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM= github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
@@ -100,8 +100,8 @@ 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.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.13/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=
@@ -128,8 +128,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.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-resty/resty/v2 v2.17.2/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=
@@ -157,14 +157,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.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
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.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA= github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA= github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
@@ -185,8 +185,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.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.4/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=
@@ -199,8 +199,8 @@ 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.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY= github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE= github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
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-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
@@ -241,10 +241,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.107.0 h1:eMzyN+jGJbxG4ut278uwIsUo9XacXc711lFjhKnaUso= github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 h1:3oOIAQ9Fd2qTKTS/VlWmvKyBPKKhXBcCXjRZqOUypI4=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8= github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
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.1 h1:2H75475moAv1hVVYlOk815KfqeiFCiQ7ovqn3OnN6FY=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0/go.mod h1:p95/OxVsdx71I2Qrck1GtIS87sRxcTRKXzUi5nWm9NY= github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1/go.mod h1:9HGOXiiQxcsG+4amgdr4xBIMq6IchdLW/nQDyZz07IE=
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=
@@ -259,14 +259,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.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg= github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
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.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U= github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pires/go-proxyproto v0.11.0/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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -293,10 +293,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.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI= github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI= github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc= github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k=
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=
@@ -335,8 +335,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.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw= github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= github.com/vultr/govultr/v3 v3.27.0/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=
@@ -344,30 +344,30 @@ 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.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34= github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtSVvg=
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34= github.com/yusing/gointernals v0.2.0/go.mod h1:xGzNbPGMm5Z8kG0t4JYISMscw+gMQlgghkLxlgRZv5Y=
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.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -376,23 +376,23 @@ 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.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.24.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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
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.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
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=
@@ -402,10 +402,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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.35.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=
@@ -434,8 +434,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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.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=
@@ -454,8 +454,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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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=
@@ -464,21 +464,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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
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.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk= google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8= google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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: e5fba76994...494ab85a33

View File

@@ -54,13 +54,13 @@ type Matchers []Matcher
### Exported functions and methods ### Exported functions and methods
```go ```go
func (c *Config) Validate() gperr.Error func (c *Config) Validate() 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) gperr.Error func (c *Config) Start(parent task.Parent) 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,6 +14,7 @@ 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"
) )
@@ -66,16 +67,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 {
@@ -89,7 +90,7 @@ const (
ACLDeny = "deny" ACLDeny = "deny"
) )
func (c *Config) Validate() gperr.Error { func (c *Config) Validate() error {
switch c.Default { switch c.Default {
case "", ACLAllow: case "", ACLAllow:
c.defaultAllow = true c.defaultAllow = true
@@ -133,7 +134,10 @@ func (c *Config) Valid() bool {
return c != nil && c.valErr == nil return c != nil && c.valErr == nil
} }
func (c *Config) Start(parent task.Parent) gperr.Error { func (c *Config) Start(parent task.Parent) 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 {
@@ -141,9 +145,6 @@ func (c *Config) Start(parent task.Parent) gperr.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)
@@ -170,13 +171,14 @@ func (c *Config) Start(parent task.Parent) gperr.Error {
return nil return nil
} }
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) { func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool, reason string) {
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(),
}) })
} }
@@ -213,23 +215,26 @@ func (c *Config) logNotifyLoop(parent task.Parent) {
select { select {
case <-parent.Context().Done(): case <-parent.Context().Done():
return return
case log := <-c.logNotifyCh: case req := <-c.logNotifyCh:
if c.logger != nil { if c.logger != nil {
if !log.allowed || c.logAllowed { if !req.allowed || c.logAllowed {
c.logger.LogACL(log.info, !log.allowed) c.logger.LogACL(req.info, !req.allowed, req.reason)
} }
} }
if c.needNotify() { if c.needNotify() {
if log.allowed { if req.allowed {
if c.notifyAllowed { if c.notifyAllowed {
c.allowedCount[log.info.Str]++ c.allowedCount[req.info.Str]++
c.totalAllowedCount++ c.totalAllowedCount++
} }
} else { } else {
c.blockedCount[log.info.Str]++ c.blockedCount[req.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 {
@@ -261,9 +266,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) { func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool, reason string) {
if c.logNotifyCh != nil { if c.logNotifyCh != nil {
c.logNotifyCh <- ipLog{info: info, allowed: allowed} c.logNotifyCh <- ipLog{info: info, allowed: allowed, reason: reason}
} }
} }
@@ -278,30 +283,36 @@ 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) c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true, "allowed by allow_local rule")
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) c.logAndNotify(record.IPInfo, record.allow, record.reason)
return record.allow return record.allow
} }
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr} ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
if c.Deny.Match(ipAndStr) { if index := c.Deny.MatchedIndex(ipAndStr); index != -1 {
c.logAndNotify(ipAndStr, false) reason := "blocked by deny rule: " + c.Deny[index].raw
c.cacheRecord(ipAndStr, false) c.logAndNotify(ipAndStr, false, reason)
c.cacheRecord(ipAndStr, false, reason)
return false return false
} }
if c.Allow.Match(ipAndStr) { if index := c.Allow.MatchedIndex(ipAndStr); index != -1 {
c.logAndNotify(ipAndStr, true) reason := "allowed by allow rule: " + c.Allow[index].raw
c.cacheRecord(ipAndStr, true) c.logAndNotify(ipAndStr, true, reason)
c.cacheRecord(ipAndStr, true, reason)
return true return true
} }
c.logAndNotify(ipAndStr, c.defaultAllow) reason := "denied by default"
c.cacheRecord(ipAndStr, c.defaultAllow) if 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,6 +2,7 @@ package acl
import ( import (
"bytes" "bytes"
"errors"
"net" "net"
"strings" "strings"
@@ -38,9 +39,9 @@ var errMatcherFormat = gperr.Multiline().AddLines(
) )
var ( var (
errSyntax = gperr.New("syntax error") errSyntax = errors.New("syntax error")
errInvalidIP = gperr.New("invalid IP") errInvalidIP = errors.New("invalid IP")
errInvalidCIDR = gperr.New("invalid CIDR") errInvalidCIDR = errors.New("invalid CIDR")
) )
func (matcher *Matcher) Parse(s string) error { func (matcher *Matcher) Parse(s string) error {
@@ -82,6 +83,15 @@ 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,6 +5,8 @@ import (
"io" "io"
"net" "net"
"time" "time"
"github.com/rs/zerolog/log"
) )
type TCPListener struct { type TCPListener struct {
@@ -44,6 +46,7 @@ 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

@@ -0,0 +1,9 @@
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

@@ -0,0 +1,16 @@
package acl
import "context"
type ContextKey struct{}
func SetCtx(ctx interface{ SetValue(any, 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,6 +4,8 @@ import (
"errors" "errors"
"net" "net"
"time" "time"
"github.com/rs/zerolog/log"
) )
type UDPListener struct { type UDPListener struct {
@@ -33,6 +35,7 @@ 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
} }
@@ -52,6 +55,7 @@ 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

@@ -19,22 +19,23 @@ 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 {
@@ -72,8 +73,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")
{ {
@@ -200,9 +201,8 @@ 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 {
gperr.LogError("Internal error", err.Err, &logger) log.Err(err.Err).Str("uri", c.Request.RequestURI).Msg("Internal error")
} }
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

@@ -2,6 +2,7 @@ package agentapi
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@@ -13,7 +14,6 @@ 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 {
@@ -84,9 +84,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 = gperr.New("agent already exists") var errAgentAlreadyExists = errors.New("agent already exists")
func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) { func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, error) {
var agentCfg agent.AgentConfig var agentCfg agent.AgentConfig
agentCfg.Addr = host agentCfg.Addr = host
agentCfg.Runtime = containerRuntime agentCfg.Runtime = containerRuntime
@@ -105,12 +105,12 @@ func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client a
err := agentCfg.InitWithCerts(ctx, ca.Cert, client.Cert, client.Key) err := agentCfg.InitWithCerts(ctx, ca.Cert, client.Cert, client.Key)
if err != nil { if err != nil {
return 0, gperr.Wrap(err, "failed to initialize agent config") return 0, fmt.Errorf("failed to initialize agent config: %w", err)
} }
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, gperr.Errorf("provider %s already exists", provider.String()) return 0, fmt.Errorf("provider %s already exists", provider.String())
} }
// agent must be added before loading routes // agent must be added before loading routes
@@ -122,7 +122,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, gperr.Wrap(err, "failed to load routes") return 0, fmt.Errorf("failed to load routes: %w", err)
} }
return provider.NumRoutes(), nil return provider.NumRoutes(), nil

View File

@@ -6,6 +6,7 @@ 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"
) )
@@ -21,7 +22,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 := autocert.ActiveProvider.Load() provider := autocertctx.FromCtx(c.Request.Context())
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"
"github.com/yusing/godoxy/internal/autocert" autocertctx "github.com/yusing/godoxy/internal/autocert/types"
"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) {
autocert := autocert.ActiveProvider.Load() provider := autocertctx.FromCtx(c.Request.Context())
if autocert == 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
} }
@@ -59,7 +59,7 @@ func Renew(c *gin.Context) {
}() }()
// renewal happens in background // renewal happens in background
ok := autocert.ForceExpiryAll() ok := provider.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")
autocert.WaitRenewalDone(manager.Context()) provider.WaitRenewalDone(manager.Context())
} }

View File

@@ -6,6 +6,8 @@ import (
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/moby/moby/api/types/container"
"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"
@@ -35,18 +37,18 @@ func Containers(c *gin.Context) {
serveHTTP[Container](c, GetContainers) serveHTTP[Container](c, GetContainers)
} }
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) { func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, error) {
errs := gperr.NewBuilder("failed to get containers") errs := gperr.NewBuilder("failed to get containers")
containers := make([]Container, 0) containers := make([]Container, 0)
for server, dockerClient := range dockerClients { for server, dockerClient := range dockerClients {
conts, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true}) conts, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true})
if err != nil { if err != nil {
errs.Add(err) errs.AddSubject(err, name)
continue continue
} }
for _, cont := range conts { for _, cont := range conts {
containers = append(containers, Container{ containers = append(containers, Container{
Server: server, Server: name,
Name: cont.Names[0], Name: cont.Names[0],
ID: cont.ID, ID: cont.ID,
Image: cont.Image, Image: cont.Image,
@@ -58,11 +60,10 @@ 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 {
gperr.LogError("failed to get containers", err) if len(containers) > 0 {
if len(containers) == 0 { log.Err(err).Msg("failed to get containers from some servers")
return nil, err return containers, nil
} }
return containers, nil
} }
return containers, nil return containers, errs.Error()
} }

View File

@@ -58,7 +58,7 @@ func Info(c *gin.Context) {
serveHTTP[dockerInfo](c, GetDockerInfo) serveHTTP[dockerInfo](c, GetDockerInfo)
} }
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, gperr.Error) { func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, 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))
@@ -66,7 +66,7 @@ func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerIn
for name, dockerClient := range dockerClients { for name, dockerClient := range dockerClients {
info, err := dockerClient.Info(ctx) info, err := dockerClient.Info(ctx)
if err != nil { if err != nil {
errs.Add(err) errs.AddSubject(err, name)
continue continue
} }
info.Name = name info.Name = name

View File

@@ -9,7 +9,7 @@ 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/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/route/routes" entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"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"
@@ -43,7 +43,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 = routes.GetIncludeExcluded(id) route, ok = entrypoint.FromCtx(c.Request.Context()).GetRoute(id)
if ok { if ok {
cont := route.ContainerInfo() cont := route.ContainerInfo()
if cont == nil { if cont == nil {

View File

@@ -8,7 +8,6 @@ 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"
) )
@@ -39,7 +38,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, gperr.Error)) { func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, error)) {
dockerClients := docker.Clients() dockerClients := docker.Clients()
defer closeAllClients(dockerClients) defer closeAllClients(dockerClients)

View File

@@ -837,6 +837,45 @@
"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",
@@ -1219,6 +1258,12 @@
"schema": { "schema": {
"$ref": "#/definitions/ErrorResponse" "$ref": "#/definitions/ErrorResponse"
} }
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
} }
}, },
"x-id": "categories", "x-id": "categories",
@@ -1337,6 +1382,12 @@
"schema": { "schema": {
"$ref": "#/definitions/ErrorResponse" "$ref": "#/definitions/ErrorResponse"
} }
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
} }
}, },
"x-id": "items", "x-id": "items",
@@ -2820,43 +2871,6 @@
"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",
@@ -3810,6 +3824,42 @@
"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": [
@@ -3979,7 +4029,6 @@
"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
@@ -3998,7 +4047,6 @@
"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
@@ -4074,7 +4122,7 @@
"HealthMap": { "HealthMap": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"$ref": "#/definitions/HealthStatusString" "$ref": "#/definitions/HealthInfoWithoutDetail"
}, },
"x-nullable": false, "x-nullable": false,
"x-omitempty": false "x-omitempty": false
@@ -5059,6 +5107,7 @@
"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
} }
@@ -5094,6 +5143,7 @@
"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
}, },
@@ -5329,7 +5379,6 @@
"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
}, },
@@ -6691,6 +6740,23 @@
"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": [

View File

@@ -295,6 +295,20 @@ 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
@@ -375,7 +389,6 @@ definitions:
HealthInfoWithoutDetail: HealthInfoWithoutDetail:
properties: properties:
latency: latency:
description: latency in microseconds
type: number type: number
status: status:
enum: enum:
@@ -387,7 +400,6 @@ definitions:
- unknown - unknown
type: string type: string
uptime: uptime:
description: uptime in milliseconds
type: number type: number
type: object type: object
HealthJSON: HealthJSON:
@@ -421,7 +433,7 @@ definitions:
type: object type: object
HealthMap: HealthMap:
additionalProperties: additionalProperties:
$ref: '#/definitions/HealthStatusString' $ref: '#/definitions/HealthInfoWithoutDetail'
type: object type: object
HealthStatusString: HealthStatusString:
enum: enum:
@@ -882,7 +894,8 @@ definitions:
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:
@@ -899,7 +912,8 @@ 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:
@@ -1007,7 +1021,6 @@ 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:
@@ -1746,6 +1759,18 @@ 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://
@@ -2452,6 +2477,31 @@ 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:
@@ -2707,6 +2757,10 @@ 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
@@ -2784,6 +2838,10 @@ 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
@@ -3790,30 +3848,6 @@ 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:

44
internal/api/v1/events.go Normal file
View File

@@ -0,0 +1,44 @@
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,7 +73,11 @@ 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
r, ok := routes.HTTP.Get(alias) ep := entrypoint.FromCtx(ctx)
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

@@ -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) gperr.Error { func validateFile(fileType FileType, content []byte) error {
switch fileType { switch fileType {
case FileTypeConfig: case FileTypeConfig:
return config.Validate(content) return config.Validate(content)

View File

@@ -5,13 +5,15 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/route/routes" entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"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"
_ "github.com/yusing/goutils/apitypes"
) )
type HealthMap = map[string]types.HealthInfoWithoutDetail // @name HealthMap
// @x-id "health" // @x-id "health"
// @BasePath /api/v1 // @BasePath /api/v1
// @Summary Get routes health info // @Summary Get routes health info
@@ -19,16 +21,21 @@ import (
// @Tags v1,websocket // @Tags v1,websocket
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} routes.HealthMap "Health info by route name" // @Success 200 {object} 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 routes.GetHealthInfoSimple(), nil return ep.GetHealthInfoWithoutDetail(), nil
}) })
} else { } else {
c.JSON(http.StatusOK, routes.GetHealthInfoSimple()) c.JSON(http.StatusOK, ep.GetHealthInfoWithoutDetail())
} }
} }

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"
_ "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
) )
// @x-id "categories" // @x-id "categories"
@@ -19,17 +19,23 @@ 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) {
c.JSON(http.StatusOK, HomepageCategories()) ep := entrypoint.FromCtx(c.Request.Context())
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() []string { func HomepageCategories(ep entrypoint.Entrypoint) []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 routes.HTTP.Iter { for _, r := range ep.HTTPRoutes().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,6 +36,7 @@ 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
@@ -53,29 +54,35 @@ 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(proto, hostname, &request), nil return HomepageItems(ep, proto, hostname, &request), nil
}) })
} else { } else {
c.JSON(http.StatusOK, HomepageItems(proto, hostname, &request)) c.JSON(http.StatusOK, HomepageItems(ep, proto, hostname, &request))
} }
} }
func HomepageItems(proto, hostname string, request *HomepageItemsRequest) homepage.Homepage { func HomepageItems(ep entrypoint.Entrypoint, 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(routes.HTTP.Size()) hp := homepage.NewHomepageMap(ep.HTTPRoutes().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 routes.HTTP.Iter { for _, r := range ep.HTTPRoutes().Iter {
if request.Provider != "" && r.ProviderName() != request.Provider { if request.Provider != "" && r.ProviderName() != request.Provider {
continue continue
} }

View File

@@ -112,7 +112,7 @@ func AllSystemInfo(c *gin.Context) {
data, err := systeminfo.Poller.GetRespData(req.Period, query) data, err := systeminfo.Poller.GetRespData(req.Period, query)
if err != nil { if err != nil {
numErrs.Add(1) numErrs.Add(1)
return gperr.PrependSubject("Main server", err) return gperr.PrependSubject(err, "Main server")
} }
select { select {
case <-manager.Done(): case <-manager.Done():
@@ -132,7 +132,7 @@ func AllSystemInfo(c *gin.Context) {
data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded) data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded)
if err != nil { if err != nil {
numErrs.Add(1) numErrs.Add(1)
return gperr.PrependSubject("Agent "+a.Name, err) return gperr.PrependSubject(err, "Agent "+a.Name)
} }
select { select {
case <-manager.Done(): case <-manager.Done():
@@ -169,7 +169,7 @@ func AllSystemInfo(c *gin.Context) {
c.Error(apitypes.InternalServerError(err, "failed to get all system info")) c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
return return
} }
gperr.LogWarn("failed to get some system info", err) log.Warn().Err(err).Msg("failed to get some system info")
} }
} }
} }

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 int `uri:"vmid" binding:"required"` VMID uint64 `uri:"vmid" binding:"required"`
} // @name ProxmoxVMActionRequest } // @name ProxmoxVMActionRequest

View File

@@ -11,10 +11,7 @@ import (
"github.com/yusing/goutils/http/websocket" "github.com/yusing/goutils/http/websocket"
) )
type StatsRequest struct { type StatsRequest ActionRequest
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

@@ -1,28 +0,0 @@
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"
_ "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
) )
type RoutesByProvider map[string][]route.Route type RoutesByProvider map[string][]route.Route
@@ -24,5 +24,10 @@ 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) {
c.JSON(http.StatusOK, routes.ByProvider()) ep := entrypoint.FromCtx(c.Request.Context())
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,6 +1,7 @@
package routeApi package routeApi
import ( import (
"fmt"
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@@ -54,16 +55,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 gperr.Error `json:"executionError,omitempty"` ExecutionError error `json:"executionError,omitempty"` // we need the structured error, not the plain string
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 gperr.Error `json:"validationError,omitempty"` ValidationError error `json:"validationError,omitempty"` // we need the structured error, not the plain string
IsResponseRule bool `json:"isResponseRule"` IsResponseRule bool `json:"isResponseRule"`
} // @name ParsedRule } // @name ParsedRule
type FinalRequest struct { type FinalRequest struct {
@@ -138,7 +139,7 @@ func Playground(c *gin.Context) {
// Execute rules // Execute rules
matchedRules := []string{} matchedRules := []string{}
upstreamCalled := false upstreamCalled := false
var executionError gperr.Error var executionError error
// Variables to capture modified request state // Variables to capture modified request state
var finalReqMethod, finalReqPath, finalReqHost string var finalReqMethod, finalReqPath, finalReqHost string
@@ -244,20 +245,22 @@ 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 *gperr.Error) { func handlerWithRecover(w http.ResponseWriter, r *http.Request, h http.HandlerFunc, outErr *error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
if outErr != nil { if outErr != nil {
*outErr = gperr.Errorf("panic during rule execution: %v", r) *outErr = fmt.Errorf("panic during rule execution: %v", r)
} }
} }
}() }()
h(w, r) h(w, r)
} }
func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, gperr.Error) { func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
var parsedRules []ParsedRule parsedRules := make([]ParsedRule, 0, len(rawRules))
var rulesList rules.Rules rulesList := make(rules.Rules, 0, len(rawRules))
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 {
@@ -284,7 +287,11 @@ func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, gperr.Error) {
// Determine if valid // Determine if valid
isValid := onErr == nil && doErr == nil isValid := onErr == nil && doErr == nil
validationErr := gperr.Join(gperr.PrependSubject("on", onErr), gperr.PrependSubject("do", doErr)) var validationErr error
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,
@@ -300,7 +307,7 @@ func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, gperr.Error) {
} }
} }
return parsedRules, rulesList, nil return parsedRules, rulesList, valErrs.Error()
} }
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 != 403 { if resp.FinalResponse.StatusCode != http.StatusForbidden {
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 != 405 { if resp.FinalResponse.StatusCode != http.StatusMethodNotAllowed {
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("POST", "/api/v1/route/playground", bytes.NewReader(body)) req := httptest.NewRequest(http.MethodPost, "/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("POST", "/api/v1/route/playground", bytes.NewReader([]byte(`{}`))) req := httptest.NewRequest(http.MethodPost, "/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"
"github.com/yusing/godoxy/internal/route/routes" entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
) )
@@ -32,7 +32,13 @@ func Route(c *gin.Context) {
return return
} }
route, ok := routes.GetIncludeExcluded(request.Which) ep := entrypoint.FromCtx(c.Request.Context())
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"
@@ -32,14 +32,16 @@ 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(routes.IterAll)) c.JSON(http.StatusOK, slices.Collect(ep.IterRoutes))
return return
} }
rts := make([]types.Route, 0, routes.NumAllRoutes()) rts := make([]types.Route, 0, ep.NumRoutes())
for r := range routes.IterAll { for r := range ep.IterRoutes {
if r.ProviderName() == provider { if r.ProviderName() == provider {
rts = append(rts, r) rts = append(rts, r)
} }
@@ -48,17 +50,19 @@ 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(routes.IterAll), nil return slices.Collect(ep.IterRoutes), 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, routes.NumAllRoutes()) rts := make([]types.Route, 0, ep.NumRoutes())
for r := range routes.IterAll { for r := range ep.IterRoutes {
if r.ProviderName() == provider { if r.ProviderName() == provider {
rts = append(rts, r) rts = append(rts, r)
} }

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) (interface{}, error) { sessionToken, err := jwt.ParseWithClaims(sessionJWT, claims, func(t *jwt.Token) (any, 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,7 +17,6 @@ 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"
@@ -76,8 +75,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 = gperr.New("missing oauth token") ErrMissingOAuthToken = errors.New("missing oauth token")
ErrInvalidOAuthToken = gperr.New("invalid oauth token") ErrInvalidOAuthToken = errors.New("invalid oauth token")
) )
// generateState generates a random string for OIDC state. // generateState generates a random string for OIDC state.

View File

@@ -2,22 +2,19 @@ package auth
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
"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 ( var ErrInvalidUsername = errors.New("invalid username")
ErrInvalidUsername = gperr.New("invalid username")
ErrInvalidPassword = gperr.New("invalid password")
)
type ( type (
UserPassAuth struct { UserPassAuth struct {
@@ -27,8 +24,9 @@ type (
tokenTTL time.Duration tokenTTL time.Duration
} }
UserPassClaims struct { UserPassClaims struct {
Username string `json:"username"`
jwt.RegisteredClaims jwt.RegisteredClaims
Username string `json:"username"`
} }
) )
@@ -81,7 +79,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) (interface{}, error) { token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (any, 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"])
} }
@@ -94,9 +92,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 ErrUserNotAllowed.Subject(claims.Username) return fmt.Errorf("%w: %s", ErrUserNotAllowed, claims.Username)
case claims.ExpiresAt.Before(time.Now()): case claims.ExpiresAt.Before(time.Now()):
return gperr.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time)) return fmt.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
} }
return nil return nil
@@ -139,11 +137,12 @@ 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 {
if user != auth.username { // always perform bcrypt comparison to avoid timing attacks
return ErrInvalidUsername.Subject(user)
}
if err := bcrypt.CompareHashAndPassword(auth.pwdHash, []byte(pass)); err != nil { if err := bcrypt.CompareHashAndPassword(auth.pwdHash, []byte(pass)); err != nil {
return ErrInvalidPassword.With(err).Subject(pass) return err
}
if user != auth.username {
return ErrInvalidUsername
} }
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, ErrInvalidPassword, err) expect.ErrorIs(t, bcrypt.ErrMismatchedHashAndPassword, 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,20 +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" strutils "github.com/yusing/goutils/strings"
) )
var ( var (
ErrMissingSessionToken = gperr.New("missing session token") ErrMissingSessionToken = errors.New("missing session token")
ErrInvalidSessionToken = gperr.New("invalid session token") ErrInvalidSessionToken = errors.New("invalid session token")
ErrUserNotAllowed = gperr.New("user not allowed") ErrUserNotAllowed = errors.New("user not allowed")
) )
func IsFrontend(r *http.Request) bool { func IsFrontend(r *http.Request) bool {

View File

@@ -66,13 +66,13 @@ const (
var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`) var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
// Validate implements the utils.CustomValidator interface. // Validate implements the serialization.CustomValidator interface.
func (cfg *Config) Validate() gperr.Error { func (cfg *Config) Validate() 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() gperr.Error { func (cfg *ConfigExtra) Validate() error {
return nil // done by main config's validate return nil // done by main config's validate
} }
@@ -80,7 +80,7 @@ func (cfg *ConfigExtra) AsConfig() *Config {
return (*Config)(cfg) return (*Config)(cfg)
} }
func (cfg *Config) validate(seenPaths map[string]int) gperr.Error { func (cfg *Config) validate(seenPaths map[string]int) error {
if cfg.Provider == "" { if cfg.Provider == "" {
cfg.Provider = ProviderLocal cfg.Provider = ProviderLocal
} }
@@ -157,7 +157,7 @@ func (cfg *Config) validate(seenPaths map[string]int) gperr.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.Add(err.Subjectf("extra[%d]", i)) b.AddSubjectf(err, "extra[%d]", i)
} }
} }
} }
@@ -179,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, gperr.New("generate ACME private key").With(err) return nil, nil, fmt.Errorf("generate ACME private key: %w", err)
} }
if err = cfg.SaveACMEKey(privKey); err != nil { if err = cfg.SaveACMEKey(privKey); err != nil {
return nil, nil, gperr.New("save ACME private key").With(err) return nil, nil, fmt.Errorf("save ACME private key: %w", err)
} }
} }
} }
@@ -206,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, gperr.New("failed to create cert pool").With(err) return nil, nil, fmt.Errorf("failed to create cert pool: %w", err)
} }
legoCfg.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = certPool legoCfg.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = certPool
} }

View File

@@ -22,6 +22,7 @@ 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"
@@ -56,15 +57,6 @@ 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
) )
@@ -82,9 +74,6 @@ 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,
@@ -119,14 +108,14 @@ func (p *Provider) GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
return p.tlsCert, nil return p.tlsCert, nil
} }
func (p *Provider) GetCertInfos() ([]CertInfo, error) { func (p *Provider) GetCertInfos() ([]autocert.CertInfo, error) {
allProviders := p.allProviders() allProviders := p.allProviders()
certInfos := make([]CertInfo, 0, len(allProviders)) certInfos := make([]autocert.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, CertInfo{ certInfos = append(certInfos, autocert.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(),
@@ -150,7 +139,7 @@ func (p *Provider) GetName() string {
} }
func (p *Provider) fmtError(err error) error { func (p *Provider) fmtError(err error) error {
return gperr.PrependSubject(fmt.Sprintf("provider: %s", p.GetName()), err) return gperr.PrependSubject(err, "provider: "+p.GetName())
} }
func (p *Provider) GetCertPath() string { func (p *Provider) GetCertPath() string {
@@ -216,7 +205,7 @@ 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 fmt.Errorf("failed to obtain cert for %s: %w", provider.GetName(), err) return gperr.PrependSubject(err, provider.GetName())
} }
return nil return nil
}) })
@@ -257,7 +246,7 @@ 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 fmt.Errorf("failed to obtain cert for %s: %w", provider.GetName(), err) return gperr.PrependSubject(err, provider.GetName())
} }
return nil return nil
}) })
@@ -475,10 +464,10 @@ func (p *Provider) scheduleRenewal(parent task.Parent) {
renewed, err := p.renew(renewMode) renewed, err := p.renew(renewMode)
if err != nil { if err != nil {
gperr.LogWarn("autocert: cert renew failed", p.fmtError(err)) log.Warn().Err(p.fmtError(err)).Msg("autocert: cert renew failed")
notif.Notify(&notif.LogMessage{ notif.Notify(&notif.LogMessage{
Level: zerolog.ErrorLevel, Level: zerolog.ErrorLevel,
Title: fmt.Sprintf("SSL certificate renewal failed for %s", p.GetName()), Title: "SSL certificate renewal failed for " + p.GetName(),
Body: notif.MessageBody(err.Error()), Body: notif.MessageBody(err.Error()),
}) })
return return
@@ -488,13 +477,13 @@ func (p *Provider) scheduleRenewal(parent task.Parent) {
notif.Notify(&notif.LogMessage{ notif.Notify(&notif.LogMessage{
Level: zerolog.InfoLevel, Level: zerolog.InfoLevel,
Title: fmt.Sprintf("SSL certificate renewed for %s", p.GetName()), Title: "SSL certificate renewed for " + 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 {
gperr.LogWarn("autocert: failed to clear last failure", p.fmtError(err)) log.Warn().Err(p.fmtError(err)).Msg("autocert: failed to clear last failure")
} }
timer.Reset(time.Until(p.ShouldRenewOn())) timer.Reset(time.Until(p.ShouldRenewOn()))
} }

View File

@@ -3,11 +3,10 @@ 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, gperr.Error) type Generator func(map[string]strutils.Redacted) (challenge.Provider, error)
var Providers = make(map[string]Generator) var Providers = make(map[string]Generator)
@@ -15,7 +14,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, gperr.Error) { return func(opt map[string]strutils.Redacted) (challenge.Provider, 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)
@@ -24,6 +23,6 @@ func DNSProvider[CT any, PT challenge.Provider](
} }
} }
p, pErr := newProvider(cfg) p, pErr := newProvider(cfg)
return p, gperr.Wrap(pErr) return p, pErr
} }
} }

View File

@@ -4,7 +4,7 @@ import (
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
) )
func (p *Provider) setupExtraProviders() gperr.Error { func (p *Provider) setupExtraProviders() 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

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,16 @@
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,14 +1,17 @@
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 {
Setup() error GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error) GetCertInfos() ([]CertInfo, error)
ScheduleRenewalAll(task.Parent) ScheduleRenewalAll(parent task.Parent)
ObtainCertAll() error ObtainCertAll() error
ForceExpiryAll() bool
WaitRenewalDone(ctx context.Context) bool
} }

View File

@@ -54,7 +54,7 @@ type State interface {
Task() *task.Task Task() *task.Task
Context() context.Context Context() context.Context
Value() *Config Value() *Config
EntrypointHandler() http.Handler Entrypoint() entrypoint.Entrypoint
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,6 +62,12 @@ 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()
} }
``` ```
@@ -214,12 +220,15 @@ 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 - `internal/entrypoint` - HTTP entrypoint setup (now via interface)
- `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
@@ -312,5 +321,8 @@ for name, provider := range config.GetState().IterProviders() {
```go ```go
state := config.GetState() state := config.GetState()
http.Handle("/", state.EntrypointHandler()) // Get entrypoint interface for route management
ep := state.Entrypoint()
// Add routes directly to entrypoint
ep.AddRoute(route)
``` ```

View File

@@ -7,14 +7,15 @@ 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"
"github.com/yusing/godoxy/internal/watcher/events" watcherEvents "github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/server" "github.com/yusing/goutils/eventqueue"
"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"
) )
@@ -26,29 +27,29 @@ var (
const configEventFlushInterval = 500 * time.Millisecond const configEventFlushInterval = 500 * time.Millisecond
const ( var (
cfgRenameWarn = `Config file renamed, not reloading. errCfgRenameWarn = errors.New("config file renamed, not reloading; Make sure you rename it back before next time you start")
Make sure you rename it back before next time you start.` errCfgDeleteWarn = errors.New(`config file deleted, not reloading; You may run "ls-config" to show or dump the current config`)
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) {
gperr.LogError("config "+action+" error", err) log.Error().Err(err).Msg("config " + action + " error")
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) {
gperr.LogWarn("config "+action+" error", err) log.Warn().Err(err).Msg("config " + action + " warning")
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 {
@@ -60,20 +61,29 @@ func Load() error {
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName) cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
// disable pool logging temporary since we already have pretty logging initErr := state.InitFromFile(common.ConfigPath)
routes.HTTP.DisableLog(true) if initErr != nil {
routes.Stream.DisableLog(true) // 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
state.Entrypoint().DisablePoolsLog(true)
defer func() { defer func() {
routes.HTTP.DisableLog(false) state.Entrypoint().DisablePoolsLog(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
@@ -81,7 +91,9 @@ func Load() error {
return nil return nil
} }
func Reload() gperr.Error { func Reload() 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()
@@ -108,32 +120,35 @@ func Reload() gperr.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() {
t := task.RootTask("config_watcher", true) opts := eventqueue.Options[watcherEvents.Event]{
eventQueue := events.NewEventQueue( FlushInterval: configEventFlushInterval,
t, OnFlush: OnConfigChange,
configEventFlushInterval, OnError: func(err error) {
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 []events.Event) { func OnConfigChange(ev []watcherEvents.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 events.ActionFileRenamed: case watcherEvents.ActionFileRenamed:
logNotifyWarn("rename", errors.New(cfgRenameWarn)) logNotifyWarn("rename", errCfgRenameWarn)
return return
case events.ActionFileDeleted: case watcherEvents.ActionFileDeleted:
logNotifyWarn("delete", errors.New(cfgDeleteWarn)) logNotifyWarn("delete", errCfgDeleteWarn)
return return
} }
@@ -142,16 +157,3 @@ func OnConfigChange(ev []events.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

@@ -9,7 +9,6 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"iter" "iter"
"net/http"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@@ -18,14 +17,21 @@ 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/yusing/godoxy/internal/acl" "github.com/rs/zerolog/log"
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"
@@ -40,7 +46,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
@@ -50,14 +56,25 @@ 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](),
entrypoint: entrypoint.NewEntrypoint(), task: task.RootTask("config", false),
task: task.RootTask("config", false), tmpLogBuf: tmpLogBuf,
tmpLogBuf: tmpLogBuf, tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf),
tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf),
} }
} }
@@ -73,13 +90,7 @@ 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 {
@@ -96,7 +107,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 err return CriticalError{err}
} }
} }
return state.Init(data) return state.Init(data)
@@ -105,7 +116,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 err return CriticalError{err}
} }
g := gperr.NewGroup("config load error") g := gperr.NewGroup("config load error")
@@ -117,7 +128,9 @@ 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())
errs.Add(state.initEntrypoint()) if err := state.initEntrypoint(); err != nil {
errs.Add(CriticalError{err})
}
errs.Add(state.loadRouteProviders()) errs.Add(state.loadRouteProviders())
return errs.Error() return errs.Error()
} }
@@ -134,8 +147,8 @@ func (state *state) Value() *config.Config {
return &state.Config return &state.Config
} }
func (state *state) EntrypointHandler() http.Handler { func (state *state) Entrypoint() entrypointctx.Entrypoint {
return &state.entrypoint return state.entrypoint
} }
func (state *state) ShortLinkMatcher() config.ShortLinkMatcher { func (state *state) ShortLinkMatcher() config.ShortLinkMatcher {
@@ -186,10 +199,39 @@ 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() {
@@ -199,7 +241,7 @@ func (state *state) initACL() error {
if err != nil { if err != nil {
return err return err
} }
state.task.SetValue(acl.ContextKey{}, state.ACL) acl.SetCtx(state.task, state.ACL)
return nil return nil
} }
@@ -207,6 +249,7 @@ 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)
@@ -220,6 +263,8 @@ 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))
@@ -296,6 +341,7 @@ func (state *state) initAutoCert() error {
p.PrintCertExpiriesAll() p.PrintCertExpiriesAll()
state.autocertProvider = p state.autocertProvider = p
autocertctx.SetCtx(state.task, p)
return nil return nil
} }
@@ -309,7 +355,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 err.Subject(cfg.URL) return gperr.PrependSubject(err, cfg.URL)
} }
return nil return nil
}) })
@@ -333,7 +379,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(a.String(), err) return gperr.PrependSubject(err, a.String())
} }
agentpool.Add(a) agentpool.Add(a)
return nil return nil
@@ -351,7 +397,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(filename, err)) errs.Add(gperr.PrependSubject(err, filename))
return err return err
} }
registerProvider(p) registerProvider(p)
@@ -376,7 +422,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 err.Subject(p.String()) return gperr.PrependSubject(err, 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,14 +8,13 @@ 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"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types" "github.com/yusing/godoxy/internal/entrypoint"
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 (
@@ -42,7 +41,7 @@ type (
} }
) )
func Validate(data []byte) gperr.Error { func Validate(data []byte) error {
var model Config var model Config
return serialization.UnmarshalValidate(data, &model, yaml.Unmarshal) return serialization.UnmarshalValidate(data, &model, yaml.Unmarshal)
} }

View File

@@ -6,6 +6,7 @@ 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"
@@ -21,7 +22,7 @@ type State interface {
Value() *Config Value() *Config
EntrypointHandler() http.Handler Entrypoint() entrypoint.Entrypoint
ShortLinkMatcher() ShortLinkMatcher ShortLinkMatcher() ShortLinkMatcher
AutoCertProvider() server.CertProvider AutoCertProvider() server.CertProvider
@@ -32,6 +33,9 @@ type State interface {
StartProviders() error StartProviders() error
FlushTmpLog() FlushTmpLog()
StartAPIServers()
StartMetrics()
} }
type ShortLinkMatcher interface { type ShortLinkMatcher interface {

View File

@@ -1,16 +1,16 @@
module github.com/yusing/godoxy/internal/dnsproviders module github.com/yusing/godoxy/internal/dnsproviders
go 1.25.6 go 1.26.0
replace github.com/yusing/godoxy => ../.. replace github.com/yusing/godoxy => ../..
require ( require (
github.com/go-acme/lego/v4 v4.31.0 github.com/go-acme/lego/v4 v4.31.0
github.com/yusing/godoxy v0.25.2 github.com/yusing/godoxy v0.26.0
) )
require ( require (
cloud.google.com/go/auth v0.18.1 // indirect cloud.google.com/go/auth v0.18.2 // 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
@@ -28,7 +28,7 @@ require (
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/fatih/structs v1.1.0 // indirect github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // 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
@@ -36,7 +36,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/go-resty/resty/v2 v2.17.1 // indirect github.com/go-resty/resty/v2 v2.17.2 // 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-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gofrs/flock v0.13.0 // indirect github.com/gofrs/flock v0.13.0 // indirect
@@ -44,15 +44,15 @@ require (
github.com/google/go-querystring v1.2.0 // indirect github.com/google/go-querystring v1.2.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.11 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/gotify/server/v2 v2.8.0 // indirect github.com/gotify/server/v2 v2.9.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/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/linode/linodego v1.64.0 // indirect github.com/linode/linodego v1.65.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/maxatome/go-testdeep v1.14.0 // indirect github.com/maxatome/go-testdeep v1.14.0 // indirect
@@ -60,8 +60,8 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/nrdcg/goacmedns v0.2.0 // indirect github.com/nrdcg/goacmedns v0.2.0 // 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.107.0 // indirect github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 // indirect github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
@@ -73,27 +73,27 @@ require (
github.com/sony/gobreaker v1.0.0 // indirect github.com/sony/gobreaker v1.0.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/objx v0.5.3 // indirect
github.com/stretchr/testify v1.11.1 // indirect github.com/stretchr/testify v1.11.1 // indirect
github.com/vultr/govultr/v3 v3.26.1 // indirect github.com/vultr/govultr/v3 v3.27.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusing/gointernals v0.1.16 // indirect github.com/yusing/gointernals v0.2.0 // indirect
github.com/yusing/goutils v0.7.0 // indirect github.com/yusing/goutils v0.7.0 // 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.64.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/crypto v0.47.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/mod v0.32.0 // indirect golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.49.0 // indirect golang.org/x/net v0.50.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect golang.org/x/tools v0.42.0 // indirect
google.golang.org/api v0.263.0 // indirect google.golang.org/api v0.266.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.78.0 // indirect google.golang.org/grpc v1.79.1 // 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

View File

@@ -1,5 +1,5 @@
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
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=
@@ -52,8 +52,8 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM= github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs= 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=
@@ -73,8 +73,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.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
@@ -95,12 +95,12 @@ 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.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA= github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA= github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
@@ -121,8 +121,8 @@ 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.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY= github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE= github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
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=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -140,10 +140,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.107.0 h1:eMzyN+jGJbxG4ut278uwIsUo9XacXc711lFjhKnaUso= github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1 h1:3oOIAQ9Fd2qTKTS/VlWmvKyBPKKhXBcCXjRZqOUypI4=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8= github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.1/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
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.1 h1:2H75475moAv1hVVYlOk815KfqeiFCiQ7ovqn3OnN6FY=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0/go.mod h1:p95/OxVsdx71I2Qrck1GtIS87sRxcTRKXzUi5nWm9NY= github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.1/go.mod h1:9HGOXiiQxcsG+4amgdr4xBIMq6IchdLW/nQDyZz07IE=
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/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
@@ -180,70 +180,70 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw= github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34= github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtSVvg=
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34= github.com/yusing/gointernals v0.2.0/go.mod h1:xGzNbPGMm5Z8kG0t4JYISMscw+gMQlgghkLxlgRZv5Y=
github.com/yusing/goutils v0.7.0 h1:I5hd8GwZ+3WZqFPK0tWqek1Q5MY6Xg29hKZcwwQi4SY= github.com/yusing/goutils v0.7.0 h1:I5hd8GwZ+3WZqFPK0tWqek1Q5MY6Xg29hKZcwwQi4SY=
github.com/yusing/goutils v0.7.0/go.mod h1:CtF/KFH4q8jkr7cvBpkaExnudE0lLu8sLe43F73Bn5Q= github.com/yusing/goutils v0.7.0/go.mod h1:CtF/KFH4q8jkr7cvBpkaExnudE0lLu8sLe43F73Bn5Q=
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.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
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/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/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.35.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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
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.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk= google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8= google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
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 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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,6 +1,7 @@
package docker package docker
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@@ -11,7 +12,7 @@ import (
strutils "github.com/yusing/goutils/strings" strutils "github.com/yusing/goutils/strings"
) )
var ErrInvalidLabel = gperr.New("invalid label") var ErrInvalidLabel = errors.New("invalid label")
const nsProxyDot = NSProxy + "." const nsProxyDot = NSProxy + "."
@@ -23,7 +24,7 @@ var refPrefixes = func() []string {
return prefixes return prefixes
}() }()
func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, gperr.Error) { func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, error) {
nestedMap := make(types.LabelMap) nestedMap := make(types.LabelMap)
errs := gperr.NewBuilder("labels error") errs := gperr.NewBuilder("labels error")
@@ -35,7 +36,7 @@ func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, g
continue continue
} }
if len(parts) == 1 { if len(parts) == 1 {
errs.Add(ErrInvalidLabel.Subject(lbl)) errs.AddSubject(ErrInvalidLabel, lbl)
continue continue
} }
parts = parts[1:] parts = parts[1:]
@@ -53,7 +54,7 @@ func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, g
// Move deeper into the nested map // Move deeper into the nested map
m, ok := currentMap[k].(types.LabelMap) m, ok := currentMap[k].(types.LabelMap)
if !ok && currentMap[k] != "" { if !ok && currentMap[k] != "" {
errs.Add(gperr.Errorf("expect mapping, got %T", currentMap[k]).Subject(lbl)) errs.AddSubject(fmt.Errorf("expect mapping, got %T", currentMap[k]), lbl)
continue continue
} else if !ok { } else if !ok {
m = make(types.LabelMap) m = make(types.LabelMap)
@@ -82,15 +83,7 @@ func ExpandWildcard(labels map[string]string, aliases ...string) {
} }
// lbl is "proxy.X..." where X is alias or wildcard // lbl is "proxy.X..." where X is alias or wildcard
rest := lbl[len(nsProxyDot):] // "X..." or "X.suffix" rest := lbl[len(nsProxyDot):] // "X..." or "X.suffix"
dotIdx := strings.IndexByte(rest, '.') alias, suffix, _ := strings.Cut(rest, ".")
var alias, suffix string
if dotIdx == -1 {
alias = rest
} else {
alias = rest[:dotIdx]
suffix = rest[dotIdx+1:]
}
if alias == WildcardAlias { if alias == WildcardAlias {
delete(labels, lbl) delete(labels, lbl)
if suffix == "" || strings.Count(value, "\n") > 1 { if suffix == "" || strings.Count(value, "\n") > 1 {
@@ -120,15 +113,10 @@ func ExpandWildcard(labels map[string]string, aliases ...string) {
continue continue
} }
rest := lbl[len(nsProxyDot):] rest := lbl[len(nsProxyDot):]
dotIdx := strings.IndexByte(rest, '.') alias, suffix, ok := strings.Cut(rest, ".")
if dotIdx == -1 { if !ok || alias == "" || alias[0] == '#' {
continue continue
} }
alias := rest[:dotIdx]
if alias[0] == '#' {
continue
}
suffix := rest[dotIdx+1:]
idx, known := aliasSet[alias] idx, known := aliasSet[alias]
if !known { if !known {

View File

@@ -1,10 +1,10 @@
# Entrypoint # Entrypoint
The entrypoint package provides the main HTTP entry point for GoDoxy, handling domain-based routing, middleware application, short link matching, and access logging. The entrypoint package provides the main HTTP entry point for GoDoxy, handling domain-based routing, middleware application, short link matching, access logging, and HTTP server lifecycle management.
## Overview ## Overview
The entrypoint package implements the primary HTTP handler that receives all incoming requests, determines the target route based on hostname, applies middleware, and forwards requests to the appropriate route handler. The entrypoint package implements the primary HTTP handler that receives all incoming requests, manages the lifecycle of HTTP servers, determines the target route based on hostname, applies middleware, and forwards requests to the appropriate route handler.
### Key Features ### Key Features
@@ -14,103 +14,350 @@ The entrypoint package implements the primary HTTP handler that receives all inc
- Access logging for all requests - Access logging for all requests
- Configurable not-found handling - Configurable not-found handling
- Per-domain route resolution - Per-domain route resolution
- HTTP server management (HTTP/HTTPS)
- Route pool abstractions via [`PoolLike`](internal/entrypoint/types/entrypoint.go:27) and [`RWPoolLike`](internal/entrypoint/types/entrypoint.go:33) interfaces
## Architecture ### Primary Consumers
```mermaid - **HTTP servers**: Per-listen-addr servers dispatch requests to routes
graph TD - **Route providers**: Register routes via [`StartAddRoute`](internal/entrypoint/routes.go:48)
A[HTTP Request] --> B[Entrypoint Handler] - **Configuration layer**: Validates and applies middleware/access-logging config
B --> C{Access Logger?}
C -->|Yes| D[Wrap Response Recorder]
C -->|No| E[Skip Logging]
D --> F[Find Route by Host] ### Non-goals
E --> F
F --> G{Route Found?} - Does not implement route discovery (delegates to providers)
G -->|Yes| H{Middleware?} - Does not handle TLS certificate management (delegates to autocert)
G -->|No| I{Short Link?} - Does not implement health checks (delegates to `internal/health/monitor`)
I -->|Yes| J[Short Link Handler] - Does not manage TCP/UDP listeners directly (only HTTP/HTTPS via `goutils/server`)
I -->|No| K{Not Found Handler?}
K -->|Yes| L[Not Found Handler]
K -->|No| M[Serve 404]
H -->|Yes| N[Apply Middleware] ### Stability
H -->|No| O[Direct Route]
N --> O
O --> P[Route ServeHTTP] Internal package with stable core interfaces. The [`Entrypoint`](internal/entrypoint/types/entrypoint.go:7) interface is the public contract.
P --> Q[Response]
L --> R[404 Response]
J --> Q
M --> R
```
## Core Components
### Entrypoint Structure
```go
type Entrypoint struct {
middleware *middleware.Middleware
notFoundHandler http.Handler
accessLogger accesslog.AccessLogger
findRouteFunc func(host string) types.HTTPRoute
shortLinkTree *ShortLinkMatcher
}
```
### Active Config
```go
var ActiveConfig atomic.Pointer[entrypoint.Config]
```
## Public API ## Public API
### Creation ### Entrypoint Interface
```go ```go
// NewEntrypoint creates a new entrypoint instance. type Entrypoint interface {
func NewEntrypoint() Entrypoint // Server capabilities
SupportProxyProtocol() bool
DisablePoolsLog(v bool)
// Route registry access
GetRoute(alias string) (types.Route, bool)
StartAddRoute(r types.Route) error
IterRoutes(yield func(r types.Route) bool)
NumRoutes() int
RoutesByProvider() map[string][]types.Route
// Route pool accessors
HTTPRoutes() PoolLike[types.HTTPRoute]
StreamRoutes() PoolLike[types.StreamRoute]
ExcludedRoutes() RWPoolLike[types.Route]
// Health info queries
GetHealthInfo() map[string]types.HealthInfo
GetHealthInfoWithoutDetail() map[string]types.HealthInfoWithoutDetail
GetHealthInfoSimple() map[string]types.HealthStatus
// Configuration
SetFindRouteDomains(domains []string)
SetMiddlewares(mws []map[string]any) error
SetNotFoundRules(rules rules.Rules)
SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) error
// Context integration
ShortLinkMatcher() *ShortLinkMatcher
}
```
### Pool Interfaces
```go
type PoolLike[Route types.Route] interface {
Get(alias string) (Route, bool)
Iter(yield func(alias string, r Route) bool)
Size() int
}
type RWPoolLike[Route types.Route] interface {
PoolLike[Route]
Add(r Route)
Del(r Route)
}
``` ```
### Configuration ### Configuration
```go ```go
// SetFindRouteDomains configures domain-based route lookup. type Config struct {
func (ep *Entrypoint) SetFindRouteDomains(domains []string) SupportProxyProtocol bool `json:"support_proxy_protocol"`
Rules struct {
// SetMiddlewares loads and configures middleware chain. NotFound rules.Rules `json:"not_found"`
func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error } `json:"rules"`
Middlewares []map[string]any `json:"middlewares"`
// SetNotFoundRules configures the not-found handler. AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
func (ep *Entrypoint) SetNotFoundRules(rules rules.Rules) }
// SetAccessLogger initializes access logging.
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) error
// ShortLinkMatcher returns the short link matcher.
func (ep *Entrypoint) ShortLinkMatcher() *ShortLinkMatcher
``` ```
### Request Handling ### Context Functions
```go ```go
// ServeHTTP is the main HTTP handler. func SetCtx(ctx interface{ SetValue(any, any) }, ep Entrypoint)
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) func FromCtx(ctx context.Context) Entrypoint
// FindRoute looks up a route by hostname.
func (ep *Entrypoint) FindRoute(s string) types.HTTPRoute
``` ```
## Usage ## Architecture
### Core Components
```mermaid
classDiagram
class Entrypoint {
+task *task.new_task
+cfg *Config
+middleware *middleware.Middleware
+notFoundHandler http.Handler
+accessLogger AccessLogger
+findRouteFunc findRouteFunc
+shortLinkMatcher *ShortLinkMatcher
+streamRoutes *pool.Pool[types.StreamRoute]
+excludedRoutes *pool.Pool[types.Route]
+servers *xsync.Map[string, *httpServer]
+SupportProxyProtocol() bool
+StartAddRoute(r) error
+IterRoutes(yield)
+HTTPRoutes() PoolLike
}
class httpServer {
+routes *pool.Pool[types.HTTPRoute]
+ServeHTTP(w, r)
+AddRoute(route)
+DelRoute(route)
+FindRoute(s) types.HTTPRoute
}
class PoolLike {
<<interface>>
+Get(alias) (Route, bool)
+Iter(yield) bool
+Size() int
}
class RWPoolLike {
<<interface>>
+PoolLike
+Add(r Route)
+Del(r Route)
}
class ShortLinkMatcher {
+fqdnRoutes *xsync.Map[string, string]
+subdomainRoutes *xsync.Map[string, struct{}]
+ServeHTTP(w, r)
+AddRoute(alias)
+DelRoute(alias)
+SetDefaultDomainSuffix(suffix)
}
Entrypoint --> httpServer : manages
Entrypoint --> ShortLinkMatcher : owns
Entrypoint --> PoolLike : HTTPRoutes()
Entrypoint --> RWPoolLike : ExcludedRoutes()
httpServer --> PoolLike : routes pool
```
### Request Processing Pipeline
```mermaid
flowchart TD
A[HTTP Request] --> B[Find Route by Host]
B --> C{Route Found?}
C -->|Yes| D{Middleware?}
C -->|No| E{Short Link?}
E -->|Yes| F[Short Link Handler]
E -->|No| G{Not Found Handler?}
G -->|Yes| H[Not Found Handler]
G -->|No| I[Serve 404]
D -->|Yes| J[Apply Middleware Chain]
D -->|No| K[Direct Route Handler]
J --> K
K --> L[Route ServeHTTP]
L --> M[Response]
F --> M
H --> N[404 Response]
I --> N
```
### Server Lifecycle
```mermaid
stateDiagram-v2
[*] --> Empty: NewEntrypoint()
Empty --> Listening: StartAddRoute()
Listening --> Listening: StartAddRoute()
Listening --> Listening: delHTTPRoute()
Listening --> [*]: Cancel()
Listening --> AddingServer: addHTTPRoute()
AddingServer --> Listening: Server starts
note right of Listening
servers map: addr -> httpServer
For HTTPS, routes are added to ProxyHTTPSAddr
Default routes added to both HTTP and HTTPS
end note
```
## Data Flow
```mermaid
sequenceDiagram
participant Client
participant httpServer
participant Entrypoint
participant Middleware
participant Route
Client->>httpServer: GET /path
httpServer->>Entrypoint: FindRoute(host)
alt Route Found
Entrypoint-->>httpServer: HTTPRoute
httpServer->>Middleware: ServeHTTP(routeHandler)
alt Has Middleware
Middleware->>Middleware: Process Chain
end
Middleware->>Route: Forward Request
Route-->>Middleware: Response
Middleware-->>httpServer: Response
else Short Link (go.example.com/alias)
httpServer->>ShortLinkMatcher: Match short code
ShortLinkMatcher-->>httpServer: Redirect
else Not Found
httpServer->>NotFoundHandler: Serve 404
NotFoundHandler-->>httpServer: 404 Page
end
httpServer-->>Client: Response
```
## Route Registry
Routes are managed per-entrypoint:
```go
// Adding a route (main entry point for providers)
if err := ep.StartAddRoute(route); err != nil {
return err
}
// Iterating all routes including excluded
ep.IterRoutes(func(r types.Route) bool {
log.Info().Str("alias", r.Name()).Msg("route")
return true // continue iteration
})
// Querying by alias
route, ok := ep.GetRoute("myapp")
// Grouping by provider
byProvider := ep.RoutesByProvider()
```
## Configuration Surface
### Config Source
Environment variables and YAML config file:
```yaml
entrypoint:
support_proxy_protocol: true
middlewares:
- rate_limit:
requests_per_second: 100
rules:
not_found:
# not-found rules configuration
access_log:
path: /var/log/godoxy/access.log
```
### Environment Variables
| Variable | Description |
| ------------------------------ | ----------------------------- |
| `PROXY_SUPPORT_PROXY_PROTOCOL` | Enable PROXY protocol support |
## Dependency and Integration Map
| Dependency | Purpose |
| ---------------------------------- | --------------------------- |
| `internal/route` | Route types and handlers |
| `internal/route/rules` | Not-found rules processing |
| `internal/logging/accesslog` | Request logging |
| `internal/net/gphttp/middleware` | Middleware chain |
| `internal/types` | Route and health types |
| `github.com/puzpuzpuz/xsync/v4` | Concurrent server map |
| `github.com/yusing/goutils/pool` | Route pool implementations |
| `github.com/yusing/goutils/task` | Lifecycle management |
| `github.com/yusing/goutils/server` | HTTP/HTTPS server lifecycle |
## Observability
### Logs
| Level | Context | Description |
| ------- | --------------------- | ----------------------- |
| `DEBUG` | `route`, `listen_url` | Route addition/removal |
| `DEBUG` | `addr`, `proto` | Server lifecycle |
| `ERROR` | `route`, `listen_url` | Server startup failures |
### Metrics
Route metrics exposed via [`GetHealthInfo`](internal/entrypoint/query.go:10) methods:
```go
// Health info for all routes
healthMap := ep.GetHealthInfo()
// {
// "myapp": {Status: "healthy", Uptime: 3600, Latency: 5ms},
// "excluded-route": {Status: "unknown", Detail: "n/a"},
// }
```
## Security Considerations
- Route lookup is read-only from route pools
- Middleware chain is applied per-request
- Proxy protocol support must be explicitly enabled
- Access logger captures request metadata before processing
- Short link matching is limited to configured domains
## Failure Modes and Recovery
| Failure | Behavior | Recovery |
| --------------------- | ------------------------------- | ---------------------------- |
| Server bind fails | Error returned, route not added | Fix port/address conflict |
| Route start fails | Route excluded, error logged | Fix route configuration |
| Middleware load fails | SetMiddlewares returns error | Fix middleware configuration |
| Context cancelled | All servers stopped gracefully | Restart entrypoint |
## Usage Examples
### Basic Setup ### Basic Setup
```go ```go
ep := entrypoint.NewEntrypoint() ep := entrypoint.NewEntrypoint(parent, &entrypoint.Config{
SupportProxyProtocol: false,
})
// Configure domain matching // Configure domain matching
ep.SetFindRouteDomains([]string{".example.com", "example.com"}) ep.SetFindRouteDomains([]string{".example.com", "example.com"})
@@ -120,7 +367,7 @@ err := ep.SetMiddlewares([]map[string]any{
{"rate_limit": map[string]any{"requests_per_second": 100}}, {"rate_limit": map[string]any{"requests_per_second": 100}},
}) })
if err != nil { if err != nil {
log.Fatal(err) return err
} }
// Configure access logging // Configure access logging
@@ -128,181 +375,58 @@ err = ep.SetAccessLogger(parent, &accesslog.RequestLoggerConfig{
Path: "/var/log/godoxy/access.log", Path: "/var/log/godoxy/access.log",
}) })
if err != nil { if err != nil {
log.Fatal(err) return err
} }
// Start server
http.ListenAndServe(":80", &ep)
``` ```
### Route Lookup Logic ### Route Querying
The entrypoint uses multiple strategies to find routes:
1. **Subdomain Matching**: For `sub.domain.com`, looks for `sub`
1. **Exact Match**: Looks for the full hostname
1. **Port Stripping**: Strips port from host if present
```go ```go
func findRouteAnyDomain(host string) types.HTTPRoute { // Iterate all routes including excluded
// Try subdomain (everything before first dot) ep.IterRoutes(func(r types.Route) bool {
idx := strings.IndexByte(host, '.') log.Info().
if idx != -1 { Str("alias", r.Name()).
target := host[:idx] Str("provider", r.ProviderName()).
if r, ok := routes.HTTP.Get(target); ok { Bool("excluded", r.ShouldExclude()).
return r Msg("route")
} return true // continue iteration
} })
// Try exact match // Get health info for all routes
if r, ok := routes.HTTP.Get(host); ok { healthMap := ep.GetHealthInfoSimple()
return r for alias, status := range healthMap {
} log.Info().Str("alias", alias).Str("status", string(status)).Msg("health")
// Try stripping port
if before, _, ok := strings.Cut(host, ":"); ok {
if r, ok := routes.HTTP.Get(before); ok {
return r
}
}
return nil
} }
``` ```
### Short Links ### Route Addition
Short links use a special `.short` domain: Routes are typically added by providers via `StartAddRoute`:
```go ```go
// Request to: https://abc.short.example.com // StartAddRoute handles route registration and server creation
// Looks for route with alias "abc" if err := ep.StartAddRoute(route); err != nil {
if strings.EqualFold(host, common.ShortLinkPrefix) { return err
// Handle short link
ep.shortLinkTree.ServeHTTP(w, r)
} }
``` ```
## Data Flow ### Context Integration
```mermaid Routes can access the entrypoint from request context:
sequenceDiagram
participant Client
participant Entrypoint
participant Middleware
participant Route
participant Logger
Client->>Entrypoint: GET /path
Entrypoint->>Entrypoint: FindRoute(host)
alt Route Found
Entrypoint->>Logger: Get ResponseRecorder
Logger-->>Entrypoint: Recorder
Entrypoint->>Middleware: ServeHTTP(routeHandler)
alt Has Middleware
Middleware->>Middleware: Process Chain
end
Middleware->>Route: Forward Request
Route-->>Middleware: Response
Middleware-->>Entrypoint: Response
else Short Link
Entrypoint->>ShortLinkTree: Match short code
ShortLinkTree-->>Entrypoint: Redirect
else Not Found
Entrypoint->>NotFoundHandler: Serve 404
NotFoundHandler-->>Entrypoint: 404 Page
end
Entrypoint->>Logger: Log Request
Logger-->>Entrypoint: Complete
Entrypoint-->>Client: Response
```
## Not-Found Handling
When no route is found, the entrypoint:
1. Attempts to serve a static error page file
1. Logs the 404 request
1. Falls back to the configured error page
1. Returns 404 status code
```go ```go
func (ep *Entrypoint) serveNotFound(w http.ResponseWriter, r *http.Request) { // Set entrypoint in context (typically during initialization)
if served := middleware.ServeStaticErrorPageFile(w, r); !served { entrypoint.SetCtx(task, ep)
log.Error().
Str("method", r.Method).
Str("url", r.URL.String()).
Str("remote", r.RemoteAddr).
Msgf("not found: %s", r.Host)
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound) // Get entrypoint from context
if ok { if ep := entrypoint.FromCtx(r.Context()); ep != nil {
w.WriteHeader(http.StatusNotFound) route, ok := ep.GetRoute("alias")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(errorPage)
} else {
http.NotFound(w, r)
}
}
} }
``` ```
## Configuration Structure ## Testing Notes
```go - Benchmark tests in [`entrypoint_benchmark_test.go`](internal/entrypoint/entrypoint_benchmark_test.go)
type Config struct { - Integration tests in [`entrypoint_test.go`](internal/entrypoint/entrypoint_test.go)
Middlewares []map[string]any `json:"middlewares"` - Mock route pools for unit testing
Rules rules.Rules `json:"rules"` - Short link tests in [`shortlink_test.go`](internal/entrypoint/shortlink_test.go)
AccessLog *accesslog.RequestLoggerConfig `json:"access_log"`
}
```
## Middleware Integration
The entrypoint supports middleware chains configured via YAML:
```yaml
entrypoint:
middlewares:
- use: rate_limit
average: 100
burst: 200
bypass:
- remote 192.168.1.0/24
- use: redirect_http
```
## Access Logging
Access logging wraps the response recorder to capture:
- Request method and URL
- Response status code
- Response size
- Request duration
- Client IP address
```go
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if ep.accessLogger != nil {
rec := accesslog.GetResponseRecorder(w)
w = rec
defer func() {
ep.accessLogger.Log(r, rec.Response())
accesslog.PutResponseRecorder(rec)
}()
}
// ... handle request
}
```
## Integration Points
The entrypoint integrates with:
- **Route Registry**: HTTP route lookup
- **Middleware**: Request processing chain
- **AccessLog**: Request logging
- **ErrorPage**: 404 error pages
- **ShortLink**: Short link handling

View File

@@ -5,11 +5,13 @@ import (
"github.com/yusing/godoxy/internal/route/rules" "github.com/yusing/godoxy/internal/route/rules"
) )
// Config defines the entrypoint configuration for proxy handling,
// including proxy protocol support, routing rules, middlewares, and access logging.
type Config struct { type Config struct {
SupportProxyProtocol bool `json:"support_proxy_protocol"` SupportProxyProtocol bool `json:"support_proxy_protocol"`
Rules struct { Rules struct {
NotFound rules.Rules `json:"not_found"` NotFound rules.Rules `json:"not_found"`
} `json:"rules"` } `json:"rules"`
Middlewares []map[string]any `json:"middlewares"` Middlewares []map[string]any `json:"middlewares"`
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"` AccessLog *accesslog.RequestLoggerConfig `json:"access_log"`
} }

View File

@@ -4,44 +4,112 @@ import (
"net/http" "net/http"
"strings" "strings"
"sync/atomic" "sync/atomic"
"testing"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types" entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/logging/accesslog" "github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/net/gphttp/middleware" "github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/net/gphttp/middleware/errorpage"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/route/rules" "github.com/yusing/godoxy/internal/route/rules"
"github.com/yusing/godoxy/internal/types" "github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/pool"
"github.com/yusing/goutils/task" "github.com/yusing/goutils/task"
) )
type HTTPRoutes interface {
Get(alias string) (types.HTTPRoute, bool)
}
type findRouteFunc func(HTTPRoutes, string) types.HTTPRoute
type Entrypoint struct { type Entrypoint struct {
middleware *middleware.Middleware task *task.Task
notFoundHandler http.Handler
accessLogger accesslog.AccessLogger cfg *Config
findRouteFunc func(host string) types.HTTPRoute
shortLinkTree *ShortLinkMatcher middleware *middleware.Middleware
notFoundHandler http.Handler
accessLogger accesslog.AccessLogger
findRouteFunc findRouteFunc
shortLinkMatcher *ShortLinkMatcher
streamRoutes *pool.Pool[types.StreamRoute]
excludedRoutes *pool.Pool[types.Route]
// this only affects future http servers creation
httpPoolDisableLog atomic.Bool
servers *xsync.Map[string, *httpServer] // listen addr -> server
} }
// nil-safe var _ entrypoint.Entrypoint = &Entrypoint{}
var ActiveConfig atomic.Pointer[entrypoint.Config]
func init() { var emptyCfg Config
// make sure it's not nil
ActiveConfig.Store(&entrypoint.Config{}) func NewTestEntrypoint(tb testing.TB, cfg *Config) *Entrypoint {
tb.Helper()
testTask := task.GetTestTask(tb)
ep := NewEntrypoint(testTask, cfg)
entrypoint.SetCtx(testTask, ep)
return ep
} }
func NewEntrypoint() Entrypoint { func NewEntrypoint(parent task.Parent, cfg *Config) *Entrypoint {
return Entrypoint{ if cfg == nil {
findRouteFunc: findRouteAnyDomain, cfg = &emptyCfg
shortLinkTree: newShortLinkTree(),
} }
ep := &Entrypoint{
task: parent.Subtask("entrypoint", false),
cfg: cfg,
findRouteFunc: findRouteAnyDomain,
shortLinkMatcher: newShortLinkMatcher(),
streamRoutes: pool.New[types.StreamRoute]("stream_routes", "stream_routes"),
excludedRoutes: pool.New[types.Route]("excluded_routes", "excluded_routes"),
servers: xsync.NewMap[string, *httpServer](),
}
return ep
}
func (ep *Entrypoint) Task() *task.Task {
return ep.task
}
func (ep *Entrypoint) SupportProxyProtocol() bool {
return ep.cfg.SupportProxyProtocol
}
func (ep *Entrypoint) DisablePoolsLog(v bool) {
ep.httpPoolDisableLog.Store(v)
// apply to all running http servers
for _, srv := range ep.servers.Range {
srv.routes.DisableLog(v)
}
// apply to other pools
ep.streamRoutes.DisableLog(v)
ep.excludedRoutes.DisableLog(v)
} }
func (ep *Entrypoint) ShortLinkMatcher() *ShortLinkMatcher { func (ep *Entrypoint) ShortLinkMatcher() *ShortLinkMatcher {
return ep.shortLinkTree return ep.shortLinkMatcher
}
func (ep *Entrypoint) HTTPRoutes() entrypoint.PoolLike[types.HTTPRoute] {
return newHTTPPoolAdapter(ep)
}
func (ep *Entrypoint) StreamRoutes() entrypoint.PoolLike[types.StreamRoute] {
return ep.streamRoutes
}
func (ep *Entrypoint) ExcludedRoutes() entrypoint.RWPoolLike[types.Route] {
return ep.excludedRoutes
}
func (ep *Entrypoint) GetServer(addr string) (HTTPServer, bool) {
return ep.servers.Load(addr)
} }
func (ep *Entrypoint) SetFindRouteDomains(domains []string) { func (ep *Entrypoint) SetFindRouteDomains(domains []string) {
@@ -74,128 +142,59 @@ func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error {
} }
func (ep *Entrypoint) SetNotFoundRules(rules rules.Rules) { func (ep *Entrypoint) SetNotFoundRules(rules rules.Rules) {
ep.notFoundHandler = rules.BuildHandler(http.HandlerFunc(ep.serveNotFound)) ep.notFoundHandler = rules.BuildHandler(serveNotFound)
} }
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) (err error) { func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) error {
if cfg == nil { if cfg == nil {
ep.accessLogger = nil ep.accessLogger = nil
return err return nil
} }
ep.accessLogger, err = accesslog.NewAccessLogger(parent, cfg) accessLogger, err := accesslog.NewAccessLogger(parent, cfg)
if err != nil { if err != nil {
return err return err
} }
ep.accessLogger = accessLogger
log.Debug().Msg("entrypoint access logger created") log.Debug().Msg("entrypoint access logger created")
return err return nil
} }
func (ep *Entrypoint) FindRoute(s string) types.HTTPRoute { func findRouteAnyDomain(routes HTTPRoutes, host string) types.HTTPRoute {
return ep.findRouteFunc(s) //nolint:modernize
}
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if ep.accessLogger != nil {
rec := accesslog.GetResponseRecorder(w)
w = rec
defer func() {
ep.accessLogger.LogRequest(r, rec.Response())
accesslog.PutResponseRecorder(rec)
}()
}
route := ep.findRouteFunc(r.Host)
switch {
case route != nil:
r = routes.WithRouteContext(r, route)
if ep.middleware != nil {
ep.middleware.ServeHTTP(route.ServeHTTP, w, r)
} else {
route.ServeHTTP(w, r)
}
case ep.tryHandleShortLink(w, r):
return
case ep.notFoundHandler != nil:
ep.notFoundHandler.ServeHTTP(w, r)
default:
ep.serveNotFound(w, r)
}
}
func (ep *Entrypoint) tryHandleShortLink(w http.ResponseWriter, r *http.Request) (handled bool) {
host := r.Host
if before, _, ok := strings.Cut(host, ":"); ok {
host = before
}
if strings.EqualFold(host, common.ShortLinkPrefix) {
if ep.middleware != nil {
ep.middleware.ServeHTTP(ep.shortLinkTree.ServeHTTP, w, r)
} else {
ep.shortLinkTree.ServeHTTP(w, r)
}
return true
}
return false
}
func (ep *Entrypoint) serveNotFound(w http.ResponseWriter, r *http.Request) {
// Why use StatusNotFound instead of StatusBadRequest or StatusBadGateway?
// On nginx, when route for domain does not exist, it returns StatusBadGateway.
// Then scraper / scanners will know the subdomain is invalid.
// With StatusNotFound, they won't know whether it's the path, or the subdomain that is invalid.
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
log.Error().
Str("method", r.Method).
Str("url", r.URL.String()).
Str("remote", r.RemoteAddr).
Msgf("not found: %s", r.Host)
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
if ok {
w.WriteHeader(http.StatusNotFound)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if _, err := w.Write(errorPage); err != nil {
log.Err(err).Msg("failed to write error page")
}
} else {
http.NotFound(w, r)
}
}
}
func findRouteAnyDomain(host string) types.HTTPRoute {
idx := strings.IndexByte(host, '.') idx := strings.IndexByte(host, '.')
if idx != -1 { if idx != -1 {
target := host[:idx] target := host[:idx]
if r, ok := routes.HTTP.Get(target); ok { if r, ok := routes.Get(target); ok {
return r return r
} }
} }
if r, ok := routes.HTTP.Get(host); ok { if r, ok := routes.Get(host); ok {
return r return r
} }
// try striping the trailing :port from the host // try striping the trailing :port from the host
if before, _, ok := strings.Cut(host, ":"); ok { if before, _, ok := strings.Cut(host, ":"); ok {
if r, ok := routes.HTTP.Get(before); ok { if r, ok := routes.Get(before); ok {
return r return r
} }
} }
return nil return nil
} }
func findRouteByDomains(domains []string) func(host string) types.HTTPRoute { func findRouteByDomains(domains []string) func(routes HTTPRoutes, host string) types.HTTPRoute {
return func(host string) types.HTTPRoute { return func(routes HTTPRoutes, host string) types.HTTPRoute {
host, _, _ = strings.Cut(host, ":") // strip the trailing :port host, _, _ = strings.Cut(host, ":") // strip the trailing :port
for _, domain := range domains { for _, domain := range domains {
if target, ok := strings.CutSuffix(host, domain); ok { if target, ok := strings.CutSuffix(host, domain); ok {
if r, ok := routes.HTTP.Get(target); ok { if r, ok := routes.Get(target); ok {
return r return r
} }
} }
} }
// fallback to exact match // fallback to exact match
if r, ok := routes.HTTP.Get(host); ok { if r, ok := routes.Get(host); ok {
return r return r
} }
return nil return nil

View File

@@ -10,12 +10,12 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/require"
"github.com/yusing/godoxy/internal/common"
. "github.com/yusing/godoxy/internal/entrypoint" . "github.com/yusing/godoxy/internal/entrypoint"
"github.com/yusing/godoxy/internal/route" "github.com/yusing/godoxy/internal/route"
"github.com/yusing/godoxy/internal/route/routes"
routeTypes "github.com/yusing/godoxy/internal/route/types" routeTypes "github.com/yusing/godoxy/internal/route/types"
"github.com/yusing/godoxy/internal/types" "github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/task"
) )
type noopResponseWriter struct { type noopResponseWriter struct {
@@ -48,9 +48,9 @@ func (t noopTransport) RoundTrip(req *http.Request) (*http.Response, error) {
} }
func BenchmarkEntrypointReal(b *testing.B) { func BenchmarkEntrypointReal(b *testing.B) {
var ep Entrypoint ep := NewTestEntrypoint(b, nil)
req := http.Request{ req := http.Request{
Method: "GET", Method: http.MethodGet,
URL: &url.URL{Path: "/", RawPath: "/"}, URL: &url.URL{Path: "/", RawPath: "/"},
Host: "test.domain.tld", Host: "test.domain.tld",
} }
@@ -77,48 +77,48 @@ func BenchmarkEntrypointReal(b *testing.B) {
b.Fatal(err) b.Fatal(err)
} }
r := &route.Route{ r, err := route.NewStartedTestRoute(b, &route.Route{
Alias: "test", Alias: "test",
Scheme: routeTypes.SchemeHTTP, Scheme: routeTypes.SchemeHTTP,
Host: host, Host: host,
Port: route.Port{Proxy: portInt}, Port: route.Port{Listening: 1000, Proxy: portInt},
HealthCheck: types.HealthCheckConfig{Disable: true}, HealthCheck: types.HealthCheckConfig{Disable: true},
} })
err = r.Validate() require.NoError(b, err)
if err != nil { require.False(b, r.ShouldExclude())
b.Fatal(err)
}
err = r.Start(task.RootTask("test", false))
if err != nil {
b.Fatal(err)
}
var w noopResponseWriter var w noopResponseWriter
server, ok := ep.GetServer(":1000")
if !ok {
b.Fatal("server not found")
}
server.ServeHTTP(&w, &req)
if w.statusCode != http.StatusOK {
b.Fatalf("status code is not 200: %d", w.statusCode)
}
if string(w.written) != "1" {
b.Fatalf("written is not 1: %s", string(w.written))
}
b.ResetTimer() b.ResetTimer()
for b.Loop() { for b.Loop() {
ep.ServeHTTP(&w, &req) server.ServeHTTP(&w, &req)
// if w.statusCode != http.StatusOK {
// b.Fatalf("status code is not 200: %d", w.statusCode)
// }
// if string(w.written) != "1" {
// b.Fatalf("written is not 1: %s", string(w.written))
// }
} }
} }
func BenchmarkEntrypoint(b *testing.B) { func BenchmarkEntrypoint(b *testing.B) {
var ep Entrypoint ep := NewTestEntrypoint(b, nil)
req := http.Request{ req := http.Request{
Method: "GET", Method: http.MethodGet,
URL: &url.URL{Path: "/", RawPath: "/"}, URL: &url.URL{Path: "/", RawPath: "/"},
Host: "test.domain.tld", Host: "test.domain.tld",
} }
ep.SetFindRouteDomains([]string{}) ep.SetFindRouteDomains([]string{})
r := &route.Route{ r, err := route.NewStartedTestRoute(b, &route.Route{
Alias: "test", Alias: "test",
Scheme: routeTypes.SchemeHTTP, Scheme: routeTypes.SchemeHTTP,
Host: "localhost", Host: "localhost",
@@ -128,29 +128,23 @@ func BenchmarkEntrypoint(b *testing.B) {
HealthCheck: types.HealthCheckConfig{ HealthCheck: types.HealthCheckConfig{
Disable: true, Disable: true,
}, },
} })
err := r.Validate() require.NoError(b, err)
if err != nil { require.False(b, r.ShouldExclude())
b.Fatal(err)
}
err = r.Start(task.RootTask("test", false)) r.(types.ReverseProxyRoute).ReverseProxy().Transport = noopTransport{}
if err != nil {
b.Fatal(err)
}
rev, ok := routes.HTTP.Get("test")
if !ok {
b.Fatal("route not found")
}
rev.(types.ReverseProxyRoute).ReverseProxy().Transport = noopTransport{}
var w noopResponseWriter var w noopResponseWriter
server, ok := ep.GetServer(common.ProxyHTTPAddr)
if !ok {
b.Fatal("server not found")
}
b.ResetTimer() b.ResetTimer()
for b.Loop() { for b.Loop() {
ep.ServeHTTP(&w, &req) server.ServeHTTP(&w, &req)
if w.statusCode != http.StatusOK { if w.statusCode != http.StatusOK {
b.Fatalf("status code is not 200: %d", w.statusCode) b.Fatalf("status code is not 200: %d", w.statusCode)
} }

View File

@@ -3,48 +3,70 @@ package entrypoint_test
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
. "github.com/yusing/godoxy/internal/entrypoint" . "github.com/yusing/godoxy/internal/entrypoint"
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" routeTypes "github.com/yusing/godoxy/internal/route/types"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/task"
expect "github.com/yusing/goutils/testing" expect "github.com/yusing/goutils/testing"
) )
var ep = NewEntrypoint() func addRoute(t *testing.T, alias string) {
t.Helper()
func addRoute(alias string) { ep := entrypoint.FromCtx(task.GetTestTask(t).Context())
routes.HTTP.Add(&route.ReveseProxyRoute{ require.NotNil(t, ep)
Route: &route.Route{
Alias: alias, _, err := route.NewStartedTestRoute(t, &route.Route{
Port: route.Port{ Alias: alias,
Proxy: 80, Scheme: routeTypes.SchemeHTTP,
}, Port: route.Port{
Listening: 1000,
Proxy: 8080,
},
HealthCheck: types.HealthCheckConfig{
Disable: true,
}, },
}) })
if err != nil {
t.Fatal(err)
}
route, ok := ep.HTTPRoutes().Get(alias)
require.True(t, ok, "route not found")
require.NotNil(t, route)
} }
func run(t *testing.T, match []string, noMatch []string) { func run(t *testing.T, ep *Entrypoint, match []string, noMatch []string) {
t.Helper() t.Helper()
t.Cleanup(routes.Clear)
t.Cleanup(func() { ep.SetFindRouteDomains(nil) }) server, ok := ep.GetServer(":1000")
require.True(t, ok, "server not found")
require.NotNil(t, server)
for _, test := range match { for _, test := range match {
t.Run(test, func(t *testing.T) { t.Run(test, func(t *testing.T) {
found := ep.FindRoute(test) route := server.FindRoute(test)
expect.NotNil(t, found) assert.NotNil(t, route)
}) })
} }
for _, test := range noMatch { for _, test := range noMatch {
t.Run(test, func(t *testing.T) { t.Run(test, func(t *testing.T) {
found := ep.FindRoute(test) found, ok := ep.HTTPRoutes().Get(test)
expect.Nil(t, found) assert.False(t, ok)
assert.Nil(t, found)
}) })
} }
} }
func TestFindRouteAnyDomain(t *testing.T) { func TestFindRouteAnyDomain(t *testing.T) {
addRoute("app1") ep := NewTestEntrypoint(t, nil)
addRoute(t, "app1")
tests := []string{ tests := []string{
"app1.com", "app1.com",
@@ -58,10 +80,12 @@ func TestFindRouteAnyDomain(t *testing.T) {
"app2.sub.domain.com", "app2.sub.domain.com",
} }
run(t, tests, testsNoMatch) run(t, ep, tests, testsNoMatch)
} }
func TestFindRouteExactHostMatch(t *testing.T) { func TestFindRouteExactHostMatch(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
tests := []string{ tests := []string{
"app2.com", "app2.com",
"app2.domain.com", "app2.domain.com",
@@ -75,19 +99,20 @@ func TestFindRouteExactHostMatch(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
addRoute(test) addRoute(t, test)
} }
run(t, tests, testsNoMatch) run(t, ep, tests, testsNoMatch)
} }
func TestFindRouteByDomains(t *testing.T) { func TestFindRouteByDomains(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
ep.SetFindRouteDomains([]string{ ep.SetFindRouteDomains([]string{
".domain.com", ".domain.com",
".sub.domain.com", ".sub.domain.com",
}) })
addRoute("app1") addRoute(t, "app1")
tests := []string{ tests := []string{
"app1.domain.com", "app1.domain.com",
@@ -103,16 +128,17 @@ func TestFindRouteByDomains(t *testing.T) {
"app2.sub.domain.com", "app2.sub.domain.com",
} }
run(t, tests, testsNoMatch) run(t, ep, tests, testsNoMatch)
} }
func TestFindRouteByDomainsExactMatch(t *testing.T) { func TestFindRouteByDomainsExactMatch(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
ep.SetFindRouteDomains([]string{ ep.SetFindRouteDomains([]string{
".domain.com", ".domain.com",
".sub.domain.com", ".sub.domain.com",
}) })
addRoute("app1.foo.bar") addRoute(t, "app1.foo.bar")
tests := []string{ tests := []string{
"app1.foo.bar", // exact match "app1.foo.bar", // exact match
@@ -126,13 +152,14 @@ func TestFindRouteByDomainsExactMatch(t *testing.T) {
"app1.sub.domain.com", "app1.sub.domain.com",
} }
run(t, tests, testsNoMatch) run(t, ep, tests, testsNoMatch)
} }
func TestFindRouteWithPort(t *testing.T) { func TestFindRouteWithPort(t *testing.T) {
t.Run("AnyDomain", func(t *testing.T) { t.Run("AnyDomain", func(t *testing.T) {
addRoute("app1") ep := NewTestEntrypoint(t, nil)
addRoute("app2.com") addRoute(t, "app1")
addRoute(t, "app2.com")
tests := []string{ tests := []string{
"app1:8080", "app1:8080",
@@ -144,16 +171,17 @@ func TestFindRouteWithPort(t *testing.T) {
"app2.co", "app2.co",
"app2.co:8080", "app2.co:8080",
} }
run(t, tests, testsNoMatch) run(t, ep, tests, testsNoMatch)
}) })
t.Run("ByDomains", func(t *testing.T) { t.Run("ByDomains", func(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
ep.SetFindRouteDomains([]string{ ep.SetFindRouteDomains([]string{
".domain.com", ".domain.com",
}) })
addRoute("app1") addRoute(t, "app1")
addRoute("app2") addRoute(t, "app2")
addRoute("app3.domain.com") addRoute(t, "app3.domain.com")
tests := []string{ tests := []string{
"app1.domain.com:8080", "app1.domain.com:8080",
@@ -169,6 +197,120 @@ func TestFindRouteWithPort(t *testing.T) {
"app3.domain.co", "app3.domain.co",
"app3.domain.co:8080", "app3.domain.co:8080",
} }
run(t, tests, testsNoMatch) run(t, ep, tests, testsNoMatch)
}) })
} }
func TestHealthInfoQueries(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
// Add routes without health monitors (default case)
addRoute(t, "app1")
addRoute(t, "app2")
// Test GetHealthInfo
t.Run("GetHealthInfo", func(t *testing.T) {
info := ep.GetHealthInfo()
expect.Equal(t, 2, len(info))
for _, health := range info {
expect.Equal(t, types.StatusUnknown, health.Status)
expect.Equal(t, "n/a", health.Detail)
}
})
// Test GetHealthInfoWithoutDetail
t.Run("GetHealthInfoWithoutDetail", func(t *testing.T) {
info := ep.GetHealthInfoWithoutDetail()
expect.Equal(t, 2, len(info))
for _, health := range info {
expect.Equal(t, types.StatusUnknown, health.Status)
}
})
// Test GetHealthInfoSimple
t.Run("GetHealthInfoSimple", func(t *testing.T) {
info := ep.GetHealthInfoSimple()
expect.Equal(t, 2, len(info))
for _, status := range info {
expect.Equal(t, types.StatusUnknown, status)
}
})
}
func TestRoutesByProvider(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
// Add routes with provider info
addRoute(t, "app1")
addRoute(t, "app2")
byProvider := ep.RoutesByProvider()
expect.Equal(t, 1, len(byProvider)) // All routes are from same implicit provider
routes, ok := byProvider[""]
expect.True(t, ok)
expect.Equal(t, 2, len(routes))
}
func TestNumRoutes(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
expect.Equal(t, 0, ep.NumRoutes())
addRoute(t, "app1")
expect.Equal(t, 1, ep.NumRoutes())
addRoute(t, "app2")
expect.Equal(t, 2, ep.NumRoutes())
}
func TestIterRoutes(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
addRoute(t, "app1")
addRoute(t, "app2")
addRoute(t, "app3")
count := 0
for r := range ep.IterRoutes {
count++
expect.NotNil(t, r)
}
expect.Equal(t, 3, count)
}
func TestGetRoute(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
// Route not found case
_, ok := ep.GetRoute("nonexistent")
expect.False(t, ok)
addRoute(t, "app1")
route, ok := ep.GetRoute("app1")
expect.True(t, ok)
expect.NotNil(t, route)
}
func TestHTTPRoutesPool(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
pool := ep.HTTPRoutes()
expect.Equal(t, 0, pool.Size())
addRoute(t, "app1")
expect.Equal(t, 1, pool.Size())
// Verify route is accessible
route, ok := pool.Get("app1")
expect.True(t, ok)
expect.NotNil(t, route)
}
func TestExcludedRoutesPool(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
excludedPool := ep.ExcludedRoutes()
expect.Equal(t, 0, excludedPool.Size())
}

View File

@@ -0,0 +1,51 @@
package entrypoint
import (
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/types"
)
// httpPoolAdapter implements the PoolLike interface for the HTTP routes.
type httpPoolAdapter struct {
ep *Entrypoint
}
func newHTTPPoolAdapter(ep *Entrypoint) httpPoolAdapter {
return httpPoolAdapter{ep: ep}
}
func (h httpPoolAdapter) Iter(yield func(alias string, route types.HTTPRoute) bool) {
for addr, srv := range h.ep.servers.Range {
// default routes are added to both HTTP and HTTPS servers, we don't need to iterate over them twice.
if addr == common.ProxyHTTPSAddr {
continue
}
for alias, route := range srv.routes.Iter {
if !yield(alias, route) {
return
}
}
}
}
func (h httpPoolAdapter) Get(alias string) (types.HTTPRoute, bool) {
for addr, srv := range h.ep.servers.Range {
if addr == common.ProxyHTTPSAddr {
continue
}
if route, ok := srv.routes.Get(alias); ok {
return route, true
}
}
return nil, false
}
func (h httpPoolAdapter) Size() (n int) {
for addr, srv := range h.ep.servers.Range {
if addr == common.ProxyHTTPSAddr {
continue
}
n += srv.routes.Size()
}
return
}

View File

@@ -0,0 +1,175 @@
package entrypoint
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/rs/zerolog/log"
acl "github.com/yusing/godoxy/internal/acl/types"
autocert "github.com/yusing/godoxy/internal/autocert/types"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/net/gphttp/middleware/errorpage"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/pool"
"github.com/yusing/goutils/server"
)
// HTTPServer is a server that listens on a given address and serves HTTP routes.
type HTTPServer interface {
Listen(addr string, proto HTTPProto) error
AddRoute(route types.HTTPRoute)
DelRoute(route types.HTTPRoute)
FindRoute(s string) types.HTTPRoute
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
type httpServer struct {
ep *Entrypoint
stopFunc func(reason any)
addr string
routes *pool.Pool[types.HTTPRoute]
}
type HTTPProto string
const (
HTTPProtoHTTP HTTPProto = "http"
HTTPProtoHTTPS HTTPProto = "https"
)
func NewHTTPServer(ep *Entrypoint) HTTPServer {
return newHTTPServer(ep)
}
func newHTTPServer(ep *Entrypoint) *httpServer {
return &httpServer{ep: ep}
}
// Listen starts the server and stop when entrypoint is stopped.
func (srv *httpServer) Listen(addr string, proto HTTPProto) error {
if srv.addr != "" {
return errors.New("server already started")
}
opts := server.Options{
Name: addr,
Handler: srv,
ACL: acl.FromCtx(srv.ep.task.Context()),
SupportProxyProtocol: srv.ep.cfg.SupportProxyProtocol,
}
switch proto {
case HTTPProtoHTTP:
opts.HTTPAddr = addr
case HTTPProtoHTTPS:
opts.HTTPSAddr = addr
opts.CertProvider = autocert.FromCtx(srv.ep.task.Context())
}
task := srv.ep.task.Subtask("http_server", false)
_, err := server.StartServer(task, opts)
if err != nil {
return err
}
srv.stopFunc = task.FinishAndWait
srv.addr = addr
srv.routes = pool.New[types.HTTPRoute](fmt.Sprintf("[%s] %s", proto, addr), "http_routes")
srv.routes.DisableLog(srv.ep.httpPoolDisableLog.Load())
return nil
}
func (srv *httpServer) Close() {
if srv.stopFunc == nil {
return
}
srv.stopFunc(nil)
}
func (srv *httpServer) AddRoute(route types.HTTPRoute) {
srv.routes.Add(route)
}
func (srv *httpServer) DelRoute(route types.HTTPRoute) {
srv.routes.Del(route)
}
func (srv *httpServer) FindRoute(s string) types.HTTPRoute {
return srv.ep.findRouteFunc(srv.routes, s)
}
func (srv *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if srv.ep.accessLogger != nil {
rec := accesslog.GetResponseRecorder(w)
w = rec
defer func() {
// there is no body to close
//nolint:bodyclose
srv.ep.accessLogger.LogRequest(r, rec.Response())
accesslog.PutResponseRecorder(rec)
}()
}
route := srv.ep.findRouteFunc(srv.routes, r.Host)
switch {
case route != nil:
r = routes.WithRouteContext(r, route)
if srv.ep.middleware != nil {
srv.ep.middleware.ServeHTTP(route.ServeHTTP, w, r)
} else {
route.ServeHTTP(w, r)
}
case srv.tryHandleShortLink(w, r):
return
case srv.ep.notFoundHandler != nil:
srv.ep.notFoundHandler.ServeHTTP(w, r)
default:
serveNotFound(w, r)
}
}
func (srv *httpServer) tryHandleShortLink(w http.ResponseWriter, r *http.Request) (handled bool) {
host := r.Host
if before, _, ok := strings.Cut(host, ":"); ok {
host = before
}
if strings.EqualFold(host, common.ShortLinkPrefix) {
if srv.ep.middleware != nil {
srv.ep.middleware.ServeHTTP(srv.ep.shortLinkMatcher.ServeHTTP, w, r)
} else {
srv.ep.shortLinkMatcher.ServeHTTP(w, r)
}
return true
}
return false
}
func serveNotFound(w http.ResponseWriter, r *http.Request) {
// Why use StatusNotFound instead of StatusBadRequest or StatusBadGateway?
// On nginx, when route for domain does not exist, it returns StatusBadGateway.
// Then scraper / scanners will know the subdomain is invalid.
// With StatusNotFound, they won't know whether it's the path, or the subdomain that is invalid.
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
log.Warn().
Str("method", r.Method).
Str("url", r.URL.String()).
Str("remote", r.RemoteAddr).
Msgf("not found: %s", r.Host)
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
if ok {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
if _, err := w.Write(errorPage); err != nil {
log.Err(err).Msg("failed to write error page")
}
} else {
http.NotFound(w, r)
}
}
}

View File

@@ -0,0 +1,92 @@
package entrypoint
import (
"github.com/yusing/godoxy/internal/types"
)
// GetHealthInfo returns a map of route name to health info.
//
// The health info is for all routes, including excluded routes.
func (ep *Entrypoint) GetHealthInfo() map[string]types.HealthInfo {
healthMap := make(map[string]types.HealthInfo, ep.NumRoutes())
for r := range ep.IterRoutes {
healthMap[r.Name()] = getHealthInfo(r)
}
return healthMap
}
// GetHealthInfoWithoutDetail returns a map of route name to health info without detail.
//
// The health info is for all routes, including excluded routes.
func (ep *Entrypoint) GetHealthInfoWithoutDetail() map[string]types.HealthInfoWithoutDetail {
healthMap := make(map[string]types.HealthInfoWithoutDetail, ep.NumRoutes())
for r := range ep.IterRoutes {
healthMap[r.Name()] = getHealthInfoWithoutDetail(r)
}
return healthMap
}
// GetHealthInfoSimple returns a map of route name to health status.
//
// The health status is for all routes, including excluded routes.
func (ep *Entrypoint) GetHealthInfoSimple() map[string]types.HealthStatus {
healthMap := make(map[string]types.HealthStatus, ep.NumRoutes())
for r := range ep.IterRoutes {
healthMap[r.Name()] = getHealthInfoSimple(r)
}
return healthMap
}
// RoutesByProvider returns a map of provider name to routes.
//
// The routes are all routes, including excluded routes.
func (ep *Entrypoint) RoutesByProvider() map[string][]types.Route {
rts := make(map[string][]types.Route)
for r := range ep.IterRoutes {
providerName := r.ProviderName()
rts[providerName] = append(rts[providerName], r)
}
return rts
}
func getHealthInfo(r types.Route) types.HealthInfo {
mon := r.HealthMonitor()
if mon == nil {
return types.HealthInfo{
HealthInfoWithoutDetail: types.HealthInfoWithoutDetail{
Status: types.StatusUnknown,
},
Detail: "n/a",
}
}
return types.HealthInfo{
HealthInfoWithoutDetail: types.HealthInfoWithoutDetail{
Status: mon.Status(),
Uptime: mon.Uptime(),
Latency: mon.Latency(),
},
Detail: mon.Detail(),
}
}
func getHealthInfoWithoutDetail(r types.Route) types.HealthInfoWithoutDetail {
mon := r.HealthMonitor()
if mon == nil {
return types.HealthInfoWithoutDetail{
Status: types.StatusUnknown,
}
}
return types.HealthInfoWithoutDetail{
Status: mon.Status(),
Uptime: mon.Uptime(),
Latency: mon.Latency(),
}
}
func getHealthInfoSimple(r types.Route) types.HealthStatus {
mon := r.HealthMonitor()
if mon == nil {
return types.StatusUnknown
}
return mon.Status()
}

View File

@@ -0,0 +1,146 @@
package entrypoint
import (
"errors"
"fmt"
"net"
"strconv"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/types"
)
func (ep *Entrypoint) IterRoutes(yield func(r types.Route) bool) {
for _, r := range ep.HTTPRoutes().Iter {
if !yield(r) {
return
}
}
for _, r := range ep.streamRoutes.Iter {
if !yield(r) {
return
}
}
for _, r := range ep.excludedRoutes.Iter {
if !yield(r) {
return
}
}
}
func (ep *Entrypoint) NumRoutes() int {
return ep.HTTPRoutes().Size() + ep.streamRoutes.Size() + ep.excludedRoutes.Size()
}
func (ep *Entrypoint) GetRoute(alias string) (types.Route, bool) {
if r, ok := ep.HTTPRoutes().Get(alias); ok {
return r, true
}
if r, ok := ep.streamRoutes.Get(alias); ok {
return r, true
}
if r, ok := ep.excludedRoutes.Get(alias); ok {
return r, true
}
return nil, false
}
func (ep *Entrypoint) StartAddRoute(r types.Route) error {
if r.ShouldExclude() {
ep.excludedRoutes.Add(r)
r.Task().OnCancel("remove_route", func() {
ep.excludedRoutes.Del(r)
})
return nil
}
switch r := r.(type) {
case types.HTTPRoute:
if err := ep.AddHTTPRoute(r); err != nil {
return err
}
ep.shortLinkMatcher.AddRoute(r.Key())
r.Task().OnCancel("remove_route", func() {
ep.delHTTPRoute(r)
ep.shortLinkMatcher.DelRoute(r.Key())
})
case types.StreamRoute:
err := r.ListenAndServe(r.Task().Context(), nil, nil)
if err != nil {
return err
}
ep.streamRoutes.Add(r)
r.Task().OnCancel("remove_route", func() {
r.Stream().Close()
ep.streamRoutes.Del(r)
})
default:
return fmt.Errorf("unknown route type: %T", r)
}
return nil
}
func getAddr(route types.HTTPRoute) (httpAddr, httpsAddr string) {
if port := route.ListenURL().Port(); port == "" || port == "0" {
host := route.ListenURL().Hostname()
if host == "" {
httpAddr = common.ProxyHTTPAddr
httpsAddr = common.ProxyHTTPSAddr
} else {
httpAddr = net.JoinHostPort(host, strconv.Itoa(common.ProxyHTTPPort))
httpsAddr = net.JoinHostPort(host, strconv.Itoa(common.ProxyHTTPSPort))
}
return httpAddr, httpsAddr
}
httpsAddr = route.ListenURL().Host
return
}
// AddHTTPRoute adds a HTTP route to the entrypoint's server.
//
// If the server does not exist, it will be created, started and return any error.
func (ep *Entrypoint) AddHTTPRoute(route types.HTTPRoute) error {
httpAddr, httpsAddr := getAddr(route)
var httpErr, httpsErr error
if httpAddr != "" {
httpErr = ep.addHTTPRoute(route, httpAddr, HTTPProtoHTTP)
}
if httpsAddr != "" {
httpsErr = ep.addHTTPRoute(route, httpsAddr, HTTPProtoHTTPS)
}
return errors.Join(httpErr, httpsErr)
}
func (ep *Entrypoint) addHTTPRoute(route types.HTTPRoute, addr string, proto HTTPProto) error {
var err error
srv, _ := ep.servers.LoadOrCompute(addr, func() (newSrv *httpServer, cancel bool) {
newSrv = newHTTPServer(ep)
err = newSrv.Listen(addr, proto)
cancel = err != nil
return
})
if err != nil {
return err
}
srv.AddRoute(route)
return nil
}
func (ep *Entrypoint) delHTTPRoute(route types.HTTPRoute) {
httpAddr, httpsAddr := getAddr(route)
if httpAddr != "" {
srv, _ := ep.servers.Load(httpAddr)
if srv != nil {
srv.DelRoute(route)
}
}
if httpsAddr != "" {
srv, _ := ep.servers.Load(httpsAddr)
if srv != nil {
srv.DelRoute(route)
}
}
// TODO: close server if no routes are left
}

View File

@@ -14,7 +14,7 @@ type ShortLinkMatcher struct {
subdomainRoutes *xsync.Map[string, struct{}] subdomainRoutes *xsync.Map[string, struct{}]
} }
func newShortLinkTree() *ShortLinkMatcher { func newShortLinkMatcher() *ShortLinkMatcher {
return &ShortLinkMatcher{ return &ShortLinkMatcher{
fqdnRoutes: xsync.NewMap[string, string](), fqdnRoutes: xsync.NewMap[string, string](),
subdomainRoutes: xsync.NewMap[string, struct{}](), subdomainRoutes: xsync.NewMap[string, struct{}](),

View File

@@ -6,18 +6,20 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yusing/godoxy/internal/common" "github.com/yusing/godoxy/internal/common"
. "github.com/yusing/godoxy/internal/entrypoint" . "github.com/yusing/godoxy/internal/entrypoint"
"github.com/yusing/goutils/task"
) )
func TestShortLinkMatcher_FQDNAlias(t *testing.T) { func TestShortLinkMatcher_FQDNAlias(t *testing.T) {
ep := NewEntrypoint() ep := NewEntrypoint(task.GetTestTask(t), nil)
matcher := ep.ShortLinkMatcher() matcher := ep.ShortLinkMatcher()
matcher.AddRoute("app.domain.com") matcher.AddRoute("app.domain.com")
t.Run("exact path", func(t *testing.T) { t.Run("exact path", func(t *testing.T) {
req := httptest.NewRequest("GET", "/app", nil) req := httptest.NewRequest(http.MethodGet, "/app", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
@@ -26,7 +28,7 @@ func TestShortLinkMatcher_FQDNAlias(t *testing.T) {
}) })
t.Run("with path remainder", func(t *testing.T) { t.Run("with path remainder", func(t *testing.T) {
req := httptest.NewRequest("GET", "/app/foo/bar", nil) req := httptest.NewRequest(http.MethodGet, "/app/foo/bar", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
@@ -35,7 +37,7 @@ func TestShortLinkMatcher_FQDNAlias(t *testing.T) {
}) })
t.Run("with query", func(t *testing.T) { t.Run("with query", func(t *testing.T) {
req := httptest.NewRequest("GET", "/app/foo?x=y&z=1", nil) req := httptest.NewRequest(http.MethodGet, "/app/foo?x=y&z=1", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
@@ -45,13 +47,13 @@ func TestShortLinkMatcher_FQDNAlias(t *testing.T) {
} }
func TestShortLinkMatcher_SubdomainAlias(t *testing.T) { func TestShortLinkMatcher_SubdomainAlias(t *testing.T) {
ep := NewEntrypoint() ep := NewEntrypoint(task.GetTestTask(t), nil)
matcher := ep.ShortLinkMatcher() matcher := ep.ShortLinkMatcher()
matcher.SetDefaultDomainSuffix(".example.com") matcher.SetDefaultDomainSuffix(".example.com")
matcher.AddRoute("app") matcher.AddRoute("app")
t.Run("exact path", func(t *testing.T) { t.Run("exact path", func(t *testing.T) {
req := httptest.NewRequest("GET", "/app", nil) req := httptest.NewRequest(http.MethodGet, "/app", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
@@ -60,7 +62,7 @@ func TestShortLinkMatcher_SubdomainAlias(t *testing.T) {
}) })
t.Run("with path remainder", func(t *testing.T) { t.Run("with path remainder", func(t *testing.T) {
req := httptest.NewRequest("GET", "/app/foo/bar", nil) req := httptest.NewRequest(http.MethodGet, "/app/foo/bar", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
@@ -70,13 +72,13 @@ func TestShortLinkMatcher_SubdomainAlias(t *testing.T) {
} }
func TestShortLinkMatcher_NotFound(t *testing.T) { func TestShortLinkMatcher_NotFound(t *testing.T) {
ep := NewEntrypoint() ep := NewEntrypoint(task.GetTestTask(t), nil)
matcher := ep.ShortLinkMatcher() matcher := ep.ShortLinkMatcher()
matcher.SetDefaultDomainSuffix(".example.com") matcher.SetDefaultDomainSuffix(".example.com")
matcher.AddRoute("app") matcher.AddRoute("app")
t.Run("missing key", func(t *testing.T) { t.Run("missing key", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil) req := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
@@ -84,7 +86,7 @@ func TestShortLinkMatcher_NotFound(t *testing.T) {
}) })
t.Run("unknown key", func(t *testing.T) { t.Run("unknown key", func(t *testing.T) {
req := httptest.NewRequest("GET", "/unknown", nil) req := httptest.NewRequest(http.MethodGet, "/unknown", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
@@ -93,7 +95,7 @@ func TestShortLinkMatcher_NotFound(t *testing.T) {
} }
func TestShortLinkMatcher_AddDelRoute(t *testing.T) { func TestShortLinkMatcher_AddDelRoute(t *testing.T) {
ep := NewEntrypoint() ep := NewEntrypoint(task.GetTestTask(t), nil)
matcher := ep.ShortLinkMatcher() matcher := ep.ShortLinkMatcher()
matcher.SetDefaultDomainSuffix(".example.com") matcher.SetDefaultDomainSuffix(".example.com")
@@ -101,13 +103,13 @@ func TestShortLinkMatcher_AddDelRoute(t *testing.T) {
matcher.AddRoute("app2.domain.com") matcher.AddRoute("app2.domain.com")
t.Run("both routes work", func(t *testing.T) { t.Run("both routes work", func(t *testing.T) {
req := httptest.NewRequest("GET", "/app1", nil) req := httptest.NewRequest(http.MethodGet, "/app1", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
assert.Equal(t, "https://app1.example.com/", w.Header().Get("Location")) assert.Equal(t, "https://app1.example.com/", w.Header().Get("Location"))
req = httptest.NewRequest("GET", "/app2.domain.com", nil) req = httptest.NewRequest(http.MethodGet, "/app2.domain.com", nil)
w = httptest.NewRecorder() w = httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
@@ -117,12 +119,12 @@ func TestShortLinkMatcher_AddDelRoute(t *testing.T) {
t.Run("delete route", func(t *testing.T) { t.Run("delete route", func(t *testing.T) {
matcher.DelRoute("app1") matcher.DelRoute("app1")
req := httptest.NewRequest("GET", "/app1", nil) req := httptest.NewRequest(http.MethodGet, "/app1", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
req = httptest.NewRequest("GET", "/app2.domain.com", nil) req = httptest.NewRequest(http.MethodGet, "/app2.domain.com", nil)
w = httptest.NewRecorder() w = httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
@@ -131,14 +133,14 @@ func TestShortLinkMatcher_AddDelRoute(t *testing.T) {
} }
func TestShortLinkMatcher_NoDefaultDomainSuffix(t *testing.T) { func TestShortLinkMatcher_NoDefaultDomainSuffix(t *testing.T) {
ep := NewEntrypoint() ep := NewEntrypoint(task.GetTestTask(t), nil)
matcher := ep.ShortLinkMatcher() matcher := ep.ShortLinkMatcher()
// no SetDefaultDomainSuffix called // no SetDefaultDomainSuffix called
t.Run("subdomain alias ignored", func(t *testing.T) { t.Run("subdomain alias ignored", func(t *testing.T) {
matcher.AddRoute("app") matcher.AddRoute("app")
req := httptest.NewRequest("GET", "/app", nil) req := httptest.NewRequest(http.MethodGet, "/app", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
@@ -148,7 +150,7 @@ func TestShortLinkMatcher_NoDefaultDomainSuffix(t *testing.T) {
t.Run("FQDN alias still works", func(t *testing.T) { t.Run("FQDN alias still works", func(t *testing.T) {
matcher.AddRoute("app.domain.com") matcher.AddRoute("app.domain.com")
req := httptest.NewRequest("GET", "/app.domain.com", nil) req := httptest.NewRequest(http.MethodGet, "/app.domain.com", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
matcher.ServeHTTP(w, req) matcher.ServeHTTP(w, req)
@@ -158,35 +160,39 @@ func TestShortLinkMatcher_NoDefaultDomainSuffix(t *testing.T) {
} }
func TestEntrypoint_ShortLinkDispatch(t *testing.T) { func TestEntrypoint_ShortLinkDispatch(t *testing.T) {
ep := NewEntrypoint() ep := NewEntrypoint(task.GetTestTask(t), nil)
ep.ShortLinkMatcher().SetDefaultDomainSuffix(".example.com") ep.ShortLinkMatcher().SetDefaultDomainSuffix(".example.com")
ep.ShortLinkMatcher().AddRoute("app") ep.ShortLinkMatcher().AddRoute("app")
server := NewHTTPServer(ep)
err := server.Listen("localhost:0", HTTPProtoHTTP)
require.NoError(t, err)
t.Run("shortlink host", func(t *testing.T) { t.Run("shortlink host", func(t *testing.T) {
req := httptest.NewRequest("GET", "/app", nil) req := httptest.NewRequest(http.MethodGet, "/app", nil)
req.Host = common.ShortLinkPrefix req.Host = common.ShortLinkPrefix
w := httptest.NewRecorder() w := httptest.NewRecorder()
ep.ServeHTTP(w, req) server.ServeHTTP(w, req)
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
assert.Equal(t, "https://app.example.com/", w.Header().Get("Location")) assert.Equal(t, "https://app.example.com/", w.Header().Get("Location"))
}) })
t.Run("shortlink host with port", func(t *testing.T) { t.Run("shortlink host with port", func(t *testing.T) {
req := httptest.NewRequest("GET", "/app", nil) req := httptest.NewRequest(http.MethodGet, "/app", nil)
req.Host = common.ShortLinkPrefix + ":8080" req.Host = common.ShortLinkPrefix + ":8080"
w := httptest.NewRecorder() w := httptest.NewRecorder()
ep.ServeHTTP(w, req) server.ServeHTTP(w, req)
assert.Equal(t, http.StatusTemporaryRedirect, w.Code) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
assert.Equal(t, "https://app.example.com/", w.Header().Get("Location")) assert.Equal(t, "https://app.example.com/", w.Header().Get("Location"))
}) })
t.Run("normal host", func(t *testing.T) { t.Run("normal host", func(t *testing.T) {
req := httptest.NewRequest("GET", "/app", nil) req := httptest.NewRequest(http.MethodGet, "/app", nil)
req.Host = "app.example.com" req.Host = "app.example.com"
w := httptest.NewRecorder() w := httptest.NewRecorder()
ep.ServeHTTP(w, req) server.ServeHTTP(w, req)
// Should not redirect, should try normal route lookup (which will 404) // Should not redirect, should try normal route lookup (which will 404)
assert.NotEqual(t, http.StatusTemporaryRedirect, w.Code) assert.NotEqual(t, http.StatusTemporaryRedirect, w.Code)

View File

@@ -0,0 +1,18 @@
package entrypoint
import (
"context"
)
type ContextKey struct{}
func SetCtx(ctx interface{ SetValue(any, any) }, ep Entrypoint) {
ctx.SetValue(ContextKey{}, ep)
}
func FromCtx(ctx context.Context) Entrypoint {
if ep, ok := ctx.Value(ContextKey{}).(Entrypoint); ok {
return ep
}
return nil
}

View File

@@ -0,0 +1,58 @@
package entrypoint
import (
"github.com/yusing/godoxy/internal/types"
)
// Entrypoint is the main HTTP entry point for the proxy: it performs domain-based
// route lookup, applies middleware, manages HTTP/HTTPS server lifecycle, and
// exposes route pools and health info. Route providers register routes via
// StartAddRoute; request handling uses the route pools to resolve targets.
type Entrypoint interface {
// SupportProxyProtocol reports whether the entrypoint is configured to accept
// PROXY protocol (v1/v2) on incoming connections. When true, servers expect
// the PROXY header before reading HTTP.
SupportProxyProtocol() bool
// DisablePoolsLog sets whether add/del logging for route pools is disabled.
// When v is true, logging for HTTP, stream, and excluded route pools is
// turned off; when false, it is turned on. Affects all existing and future
// pool operations until called again.
DisablePoolsLog(v bool)
GetRoute(alias string) (types.Route, bool)
// StartAddRoute registers the route with the entrypoint. It is synchronous:
// it does not return until the route is registered or an error occurs. For
// HTTP routes, a server for the route's listen address is created and
// started if needed. For stream routes, ListenAndServe is invoked and the
// route is added to the pool only on success. Excluded routes are added to
// the excluded pool only. Returns an error on listen/bind failure, stream
// listen failure, or unsupported route type.
StartAddRoute(r types.Route) error
IterRoutes(yield func(r types.Route) bool)
NumRoutes() int
RoutesByProvider() map[string][]types.Route
// HTTPRoutes returns a read-only view of all HTTP routes (across listen addrs).
HTTPRoutes() PoolLike[types.HTTPRoute]
// StreamRoutes returns a read-only view of all stream (e.g. TCP/UDP) routes.
StreamRoutes() PoolLike[types.StreamRoute]
// ExcludedRoutes returns the read-write pool of excluded routes (e.g. disabled).
ExcludedRoutes() RWPoolLike[types.Route]
GetHealthInfo() map[string]types.HealthInfo
GetHealthInfoWithoutDetail() map[string]types.HealthInfoWithoutDetail
GetHealthInfoSimple() map[string]types.HealthStatus
}
type PoolLike[Route types.Route] interface {
Get(alias string) (Route, bool)
Iter(yield func(alias string, r Route) bool)
Size() int
}
type RWPoolLike[Route types.Route] interface {
PoolLike[Route]
Add(r Route)
Del(r Route)
}

View File

@@ -15,21 +15,23 @@ import (
type DockerHealthcheckState struct { type DockerHealthcheckState struct {
client *docker.SharedClient client *docker.SharedClient
containerId string containerID string
numDockerFailures int numDockerFailures int
} }
const dockerFailuresThreshold = 3 const dockerFailuresThreshold = 3
var ErrDockerHealthCheckFailedTooManyTimes = errors.New("docker health check failed too many times") var (
var ErrDockerHealthCheckNotAvailable = errors.New("docker health check not available") ErrDockerHealthCheckFailedTooManyTimes = errors.New("docker health check failed too many times")
ErrDockerHealthCheckNotAvailable = errors.New("docker health check not available")
)
func NewDockerHealthcheckState(client *docker.SharedClient, containerId string) *DockerHealthcheckState { func NewDockerHealthcheckState(client *docker.SharedClient, containerID string) *DockerHealthcheckState {
client.InterceptHTTPClient(interceptDockerInspectResponse) client.InterceptHTTPClient(interceptDockerInspectResponse)
return &DockerHealthcheckState{ return &DockerHealthcheckState{
client: client, client: client,
containerId: containerId, containerID: containerID,
numDockerFailures: 0, numDockerFailures: 0,
} }
} }
@@ -43,7 +45,7 @@ func Docker(ctx context.Context, state *DockerHealthcheckState, timeout time.Dur
defer cancel() defer cancel()
// the actual inspect response is intercepted and returned as RequestInterceptedError // the actual inspect response is intercepted and returned as RequestInterceptedError
_, err := state.client.ContainerInspect(ctx, state.containerId) _, err := state.client.ContainerInspect(ctx, state.containerID)
var interceptedErr *httputils.RequestInterceptedError var interceptedErr *httputils.RequestInterceptedError
if !httputils.AsRequestInterceptedError(err, &interceptedErr) { if !httputils.AsRequestInterceptedError(err, &interceptedErr) {

View File

@@ -31,7 +31,7 @@ var pinger = &fasthttp.Client{
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
}, },
MaxConnsPerHost: 1, MaxConnsPerHost: 1000,
NoDefaultUserAgentHeader: true, NoDefaultUserAgentHeader: true,
} }
@@ -45,7 +45,7 @@ func HTTP(url *url.URL, method, path string, timeout time.Duration) (types.Healt
req.SetRequestURI(url.JoinPath(path).String()) req.SetRequestURI(url.JoinPath(path).String())
req.Header.SetMethod(method) req.Header.SetMethod(method)
setCommonHeaders(req.Header.Set) setCommonHeaders(req.Header.Set)
req.SetConnectionClose() // req.SetConnectionClose() // allow connection reuse
start := time.Now() start := time.Now()
respErr := pinger.DoTimeout(req, resp, timeout) respErr := pinger.DoTimeout(req, resp, timeout)

View File

@@ -41,7 +41,7 @@ type HealthCheckFunc func(url *url.URL) (result types.HealthCheckResult, err err
```go ```go
type HealthMonitor interface { type HealthMonitor interface {
Start(parent task.Parent) gperr.Error Start(parent task.Parent) error
Task() *task.Task Task() *task.Task
Finish(reason any) Finish(reason any)
UpdateURL(url *url.URL) UpdateURL(url *url.URL)

View File

@@ -2,6 +2,7 @@ package monitor
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"math/rand" "math/rand"
"net/url" "net/url"
@@ -13,13 +14,15 @@ import (
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/types" "github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs" "github.com/yusing/goutils/events"
strutils "github.com/yusing/goutils/strings" strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/synk" "github.com/yusing/goutils/synk"
"github.com/yusing/goutils/task" "github.com/yusing/goutils/task"
) )
type ( type (
DisplayNameKey struct{}
HealthCheckFunc func(url *url.URL) (result types.HealthCheckResult, err error) HealthCheckFunc func(url *url.URL) (result types.HealthCheckResult, err error)
monitor struct { monitor struct {
service string service string
@@ -42,7 +45,7 @@ type (
} }
) )
var ErrNegativeInterval = gperr.New("negative interval") var ErrNegativeInterval = errors.New("negative interval")
func (mon *monitor) init(u *url.URL, cfg types.HealthCheckConfig, healthCheckFunc HealthCheckFunc) { func (mon *monitor) init(u *url.URL, cfg types.HealthCheckConfig, healthCheckFunc HealthCheckFunc) {
if state := config.WorkingState.Load(); state != nil { if state := config.WorkingState.Load(); state != nil {
@@ -79,14 +82,18 @@ func (mon *monitor) CheckHealth() (types.HealthCheckResult, error) {
} }
// Start implements task.TaskStarter. // Start implements task.TaskStarter.
func (mon *monitor) Start(parent task.Parent) gperr.Error { func (mon *monitor) Start(parent task.Parent) error {
if mon.config.Interval <= 0 { if mon.config.Interval <= 0 {
return ErrNegativeInterval return ErrNegativeInterval
} }
mon.service = parent.Name()
mon.task = parent.Subtask("health_monitor", true) mon.task = parent.Subtask("health_monitor", true)
mon.service = parent.Name()
if displayName, ok := parent.GetValue(DisplayNameKey{}).(string); ok {
mon.service = displayName
}
go func() { go func() {
logger := log.With().Str("name", mon.service).Logger() logger := log.With().Str("name", mon.service).Logger()
@@ -269,6 +276,7 @@ func (mon *monitor) notifyServiceUp(logger *zerolog.Logger, result *types.Health
Body: extras, Body: extras,
Color: notif.ColorSuccess, Color: notif.ColorSuccess,
}) })
events.Global.Add(events.NewEvent(events.LevelInfo, "health", "service_up", mon))
} }
func (mon *monitor) notifyServiceDown(logger *zerolog.Logger, result *types.HealthCheckResult) { func (mon *monitor) notifyServiceDown(logger *zerolog.Logger, result *types.HealthCheckResult) {
@@ -281,6 +289,7 @@ func (mon *monitor) notifyServiceDown(logger *zerolog.Logger, result *types.Heal
Body: extras, Body: extras,
Color: notif.ColorError, Color: notif.ColorError,
}) })
events.Global.Add(events.NewEvent(events.LevelWarn, "health", "service_down", mon))
} }
func (mon *monitor) buildNotificationExtras(result *types.HealthCheckResult) notif.FieldsBody { func (mon *monitor) buildNotificationExtras(result *types.HealthCheckResult) notif.FieldsBody {

View File

@@ -2,9 +2,9 @@ package monitor
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@@ -14,8 +14,10 @@ import (
"github.com/yusing/godoxy/internal/types" "github.com/yusing/godoxy/internal/types"
) )
type Result = types.HealthCheckResult type (
type Monitor = types.HealthMonCheck Result = types.HealthCheckResult
Monitor = types.HealthMonCheck
)
// NewMonitor creates a health monitor based on the route type and configuration. // NewMonitor creates a health monitor based on the route type and configuration.
// //
@@ -78,22 +80,22 @@ func NewFileServerHealthMonitor(config types.HealthCheckConfig, path string) Mon
return &mon return &mon
} }
func NewStreamHealthMonitor(config types.HealthCheckConfig, targetUrl *url.URL) Monitor { func NewStreamHealthMonitor(config types.HealthCheckConfig, targetURL *url.URL) Monitor {
var mon monitor var mon monitor
mon.init(targetUrl, config, func(u *url.URL) (result Result, err error) { mon.init(targetURL, config, func(u *url.URL) (result Result, err error) {
return healthcheck.Stream(mon.Context(), u, config.Timeout) return healthcheck.Stream(mon.Context(), u, config.Timeout)
}) })
return &mon return &mon
} }
func NewDockerHealthMonitor(config types.HealthCheckConfig, client *docker.SharedClient, containerId string, fallback Monitor) Monitor { func NewDockerHealthMonitor(config types.HealthCheckConfig, client *docker.SharedClient, containerID string, fallback Monitor) Monitor {
state := healthcheck.NewDockerHealthcheckState(client, containerId) state := healthcheck.NewDockerHealthcheckState(client, containerID)
displayURL := &url.URL{ // only for display purposes, no actual request is made displayURL := &url.URL{ // only for display purposes, no actual request is made
Scheme: "docker", Scheme: "docker",
Host: client.DaemonHost(), Host: client.DaemonHost(),
Path: "/containers/" + containerId + "/json", Path: "/containers/" + containerID + "/json",
} }
logger := log.With().Str("host", client.DaemonHost()).Str("container_id", containerId).Logger() logger := log.With().Str("host", client.DaemonHost()).Str("container_id", containerID).Logger()
isFirstFailure := true isFirstFailure := true
var mon monitor var mon monitor
@@ -114,20 +116,20 @@ func NewDockerHealthMonitor(config types.HealthCheckConfig, client *docker.Share
return &mon return &mon
} }
func NewAgentProxiedMonitor(config types.HealthCheckConfig, agent *agentpool.Agent, targetUrl *url.URL) Monitor { func NewAgentProxiedMonitor(config types.HealthCheckConfig, agent *agentpool.Agent, targetURL *url.URL) Monitor {
var mon monitor var mon monitor
mon.init(targetUrl, config, func(u *url.URL) (result Result, err error) { mon.init(targetURL, config, func(u *url.URL) (result Result, err error) {
return CheckHealthAgentProxied(agent, config.Timeout, u) return CheckHealthAgentProxied(agent, config.Timeout, u)
}) })
return &mon return &mon
} }
func CheckHealthAgentProxied(agent *agentpool.Agent, timeout time.Duration, targetUrl *url.URL) (Result, error) { func CheckHealthAgentProxied(agent *agentpool.Agent, timeout time.Duration, targetURL *url.URL) (Result, error) {
query := url.Values{ query := url.Values{
"scheme": {targetUrl.Scheme}, "scheme": {targetURL.Scheme},
"host": {targetUrl.Host}, "host": {targetURL.Host},
"path": {targetUrl.Path}, "path": {targetURL.Path},
"timeout": {fmt.Sprintf("%d", timeout.Milliseconds())}, "timeout": {strconv.FormatInt(timeout.Milliseconds(), 10)},
} }
resp, err := agent.DoHealthCheck(timeout, query.Encode()) resp, err := agent.DoHealthCheck(timeout, query.Encode())
result := Result{ result := Result{

View File

@@ -176,7 +176,7 @@ func (icon *Meta) Filenames(ref string) []string
func NewURL(source Source, refOrName, format string) *URL func NewURL(source Source, refOrName, format string) *URL
// ErrInvalidIconURL is returned when icon URL parsing fails // ErrInvalidIconURL is returned when icon URL parsing fails
var ErrInvalidIconURL = gperr.New("invalid icon url") var ErrInvalidIconURL = errors.New("invalid icon url")
``` ```
### Provider Interface ### Provider Interface

View File

@@ -137,11 +137,11 @@ func fetchIcon(ctx context.Context, filename string) (Result, error) {
for _, fileType := range []string{"svg", "webp", "png"} { for _, fileType := range []string{"svg", "webp", "png"} {
result, err := fetchKnownIcon(ctx, icons.NewURL(icons.SourceSelfhSt, filename, fileType)) result, err := fetchKnownIcon(ctx, icons.NewURL(icons.SourceSelfhSt, filename, fileType))
if err == nil { if err == nil {
return result, err return result, nil
} }
result, err = fetchKnownIcon(ctx, icons.NewURL(icons.SourceWalkXCode, filename, fileType)) result, err = fetchKnownIcon(ctx, icons.NewURL(icons.SourceWalkXCode, filename, fileType))
if err == nil { if err == nil {
return result, err return result, nil
} }
} }
return FetchResultWithErrorf(http.StatusNotFound, "no icon found") return FetchResultWithErrorf(http.StatusNotFound, "no icon found")
@@ -152,6 +152,8 @@ type contextValue struct {
uri string uri string
} }
type contextKey struct{}
func FindIcon(ctx context.Context, r route, uri string, variant icons.Variant) (Result, error) { func FindIcon(ctx context.Context, r route, uri string, variant icons.Variant) (Result, error) {
for _, ref := range r.References() { for _, ref := range r.References() {
ref = sanitizeName(ref) ref = sanitizeName(ref)
@@ -160,7 +162,7 @@ func FindIcon(ctx context.Context, r route, uri string, variant icons.Variant) (
} }
result, err := fetchIcon(ctx, ref) result, err := fetchIcon(ctx, ref)
if err == nil { if err == nil {
return result, err return result, nil
} }
} }
if r, ok := r.(httpRoute); ok { if r, ok := r.(httpRoute); ok {
@@ -168,13 +170,13 @@ func FindIcon(ctx context.Context, r route, uri string, variant icons.Variant) (
return FetchResultWithErrorf(http.StatusServiceUnavailable, "service unavailable") return FetchResultWithErrorf(http.StatusServiceUnavailable, "service unavailable")
} }
// fallback to parse html // fallback to parse html
return findIconSlowCached(context.WithValue(ctx, "route", contextValue{r: r, uri: uri}), r.Key()) return findIconSlowCached(context.WithValue(ctx, contextKey{}, contextValue{r: r, uri: uri}), r.Key())
} }
return FetchResultWithErrorf(http.StatusNotFound, "no icon found") return FetchResultWithErrorf(http.StatusNotFound, "no icon found")
} }
var findIconSlowCached = cache.NewKeyFunc(func(ctx context.Context, key string) (Result, error) { var findIconSlowCached = cache.NewKeyFunc(func(ctx context.Context, key string) (Result, error) {
v := ctx.Value("route").(contextValue) v := ctx.Value(contextKey{}).(contextValue)
return findIconSlow(ctx, v.r, v.uri, nil) return findIconSlow(ctx, v.r, v.uri, nil)
}).WithMaxEntries(200).WithRetriesConstantBackoff(math.MaxInt, 15*time.Second).Build() // infinite retries, 15 seconds interval }).WithMaxEntries(200).WithRetriesConstantBackoff(math.MaxInt, 15*time.Second).Build() // infinite retries, 15 seconds interval

View File

@@ -1,6 +1,10 @@
package icons package icons
import "sync/atomic" import (
"sync/atomic"
"github.com/yusing/godoxy/internal/common"
)
type Provider interface { type Provider interface {
HasIcon(u *URL) bool HasIcon(u *URL) bool
@@ -13,6 +17,9 @@ func SetProvider(p Provider) {
} }
func hasIcon(u *URL) bool { func hasIcon(u *URL) bool {
if common.IsTest {
return true
}
v := provider.Load() v := provider.Load()
if v == nil { if v == nil {
return false return false

View File

@@ -1,6 +1,7 @@
package icons package icons
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
@@ -40,7 +41,7 @@ const (
VariantDark Variant = "dark" VariantDark Variant = "dark"
) )
var ErrInvalidIconURL = gperr.New("invalid icon url") var ErrInvalidIconURL = errors.New("invalid icon url")
func NewURL(source Source, refOrName, format string) *URL { func NewURL(source Source, refOrName, format string) *URL {
switch source { switch source {
@@ -119,7 +120,7 @@ func (u *URL) parse(v string, checkExists bool) error {
case "@target", "": // @target/favicon.ico, /favicon.ico case "@target", "": // @target/favicon.ico, /favicon.ico
url := v[slashIndex:] url := v[slashIndex:]
if url == "/" { if url == "/" {
return ErrInvalidIconURL.Withf("%s", "empty path") return fmt.Errorf("%w: empty path", ErrInvalidIconURL)
} }
u.FullURL = &url u.FullURL = &url
u.Source = SourceRelative u.Source = SourceRelative
@@ -131,7 +132,7 @@ func (u *URL) parse(v string, checkExists bool) error {
} }
parts := strings.Split(v[slashIndex+1:], ".") parts := strings.Split(v[slashIndex+1:], ".")
if len(parts) != 2 { if len(parts) != 2 {
return ErrInvalidIconURL.Withf("expect @%s/<reference>.<format>, e.g. @%s/adguard-home.webp", beforeSlash, beforeSlash) return fmt.Errorf("%w: expect %s/<reference>.<format>, e.g. %s/adguard-home.webp", ErrInvalidIconURL, beforeSlash, beforeSlash)
} }
reference, format := parts[0], strings.ToLower(parts[1]) reference, format := parts[0], strings.ToLower(parts[1])
if reference == "" || format == "" { if reference == "" || format == "" {
@@ -140,7 +141,7 @@ func (u *URL) parse(v string, checkExists bool) error {
switch format { switch format {
case "svg", "png", "webp": case "svg", "png", "webp":
default: default:
return ErrInvalidIconURL.Withf("%s", "invalid image format, expect svg/png/webp") return fmt.Errorf("%w: invalid image format, expect svg/png/webp", ErrInvalidIconURL)
} }
isLight, isDark := false, false isLight, isDark := false, false
if strings.HasSuffix(reference, "-light") { if strings.HasSuffix(reference, "-light") {
@@ -158,10 +159,10 @@ func (u *URL) parse(v string, checkExists bool) error {
IsDark: isDark, IsDark: isDark,
} }
if checkExists && !u.HasIcon() { if checkExists && !u.HasIcon() {
return ErrInvalidIconURL.Withf("no such icon %s.%s from %s", reference, format, u.Source) return fmt.Errorf("%w: no such icon %s.%s from %s", ErrInvalidIconURL, reference, format, u.Source)
} }
default: default:
return ErrInvalidIconURL.Subject(v) return gperr.PrependSubject(ErrInvalidIconURL, v)
} }
return nil return nil

View File

@@ -3,12 +3,12 @@ package qbittorrent
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"github.com/yusing/godoxy/internal/homepage/widgets" "github.com/yusing/godoxy/internal/homepage/widgets"
gperr "github.com/yusing/goutils/errs"
) )
type Client struct { type Client struct {
@@ -46,7 +46,7 @@ func (c *Client) doRequest(ctx context.Context, method, endpoint string, query u
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, gperr.Errorf("%w: %d %s", widgets.ErrHTTPStatus, resp.StatusCode, resp.Status) return nil, fmt.Errorf("%w: %d %s", widgets.ErrHTTPStatus, resp.StatusCode, resp.Status)
} }
return resp, nil return resp, nil

View File

@@ -50,8 +50,7 @@ const (
### Errors ### Errors
```go ```go
var ErrInvalidProvider = gperr.New("invalid provider") var ErrInvalidProvider = errors.New("invalid provider")
var ErrHTTPStatus = gperr.New("http status")
``` ```
## API Reference ## API Reference

View File

@@ -1,14 +1,13 @@
package widgets package widgets
import ( import (
"errors"
"net/http" "net/http"
"time" "time"
gperr "github.com/yusing/goutils/errs"
) )
var HTTPClient = &http.Client{ var HTTPClient = &http.Client{
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
} }
var ErrHTTPStatus = gperr.New("http status") var ErrHTTPStatus = errors.New("http status")

View File

@@ -2,6 +2,8 @@ package widgets
import ( import (
"context" "context"
"errors"
"fmt"
"github.com/yusing/godoxy/internal/serialization" "github.com/yusing/godoxy/internal/serialization"
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
@@ -30,21 +32,21 @@ var widgetProviders = map[string]struct{}{
WidgetProviderQbittorrent: {}, WidgetProviderQbittorrent: {},
} }
var ErrInvalidProvider = gperr.New("invalid provider") var ErrInvalidProvider = errors.New("invalid provider")
func (cfg *Config) UnmarshalMap(m map[string]any) error { func (cfg *Config) UnmarshalMap(m map[string]any) error {
var ok bool var ok bool
cfg.Provider, ok = m["provider"].(string) cfg.Provider, ok = m["provider"].(string)
if !ok { if !ok {
return ErrInvalidProvider.Withf("non string") return fmt.Errorf("%w: non string", ErrInvalidProvider)
} }
if _, ok := widgetProviders[cfg.Provider]; !ok { if _, ok := widgetProviders[cfg.Provider]; !ok {
return ErrInvalidProvider.Subject(cfg.Provider) return gperr.PrependSubject(ErrInvalidProvider, cfg.Provider)
} }
delete(m, "provider") delete(m, "provider")
m, ok = m["config"].(map[string]any) m, ok = m["config"].(map[string]any)
if !ok { if !ok {
return gperr.New("invalid config") return errors.New("invalid config")
} }
return serialization.MapUnmarshalValidate(m, &cfg.Config) return serialization.MapUnmarshalValidate(m, &cfg.Config)
} }

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