mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-22 16:28:30 +02:00
v0.26.0
This commit is contained in:
16
.gitignore
vendored
16
.gitignore
vendored
@@ -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.*
|
||||||
@@ -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
|
||||||
|
|||||||
2
.trunk/configs/.markdownlint.yaml
Normal file
2
.trunk/configs/.markdownlint.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Prettier friendly markdownlint config (all formatting rules disabled)
|
||||||
|
extends: markdownlint/style/prettier
|
||||||
7
.trunk/configs/.yamllint.yaml
Normal file
7
.trunk/configs/.yamllint.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
rules:
|
||||||
|
quoted-strings:
|
||||||
|
required: only-when-needed
|
||||||
|
extra-allowed: ["{|}"]
|
||||||
|
key-duplicates: {}
|
||||||
|
octal-values:
|
||||||
|
forbid-implicit-octal: true
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
40
Makefile
40
Makefile
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
38
agent/go.mod
38
agent/go.mod
@@ -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
|
||||||
|
|||||||
100
agent/go.sum
100
agent/go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.25.6-alpine AS builder
|
FROM golang:1.26.0-alpine AS builder
|
||||||
|
|
||||||
HEALTHCHECK NONE
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.25.6-alpine AS builder
|
FROM golang:1.26.0-alpine AS builder
|
||||||
|
|
||||||
HEALTHCHECK NONE
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
31
cmd/main.go
31
cmd/main.go
@@ -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)
|
||||||
|
|||||||
@@ -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
78
go.mod
@@ -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
144
go.sum
@@ -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=
|
||||||
|
|||||||
2
goutils
2
goutils
Submodule goutils updated: e5fba76994...494ab85a33
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
9
internal/acl/types/acl.go
Normal file
9
internal/acl/types/acl.go
Normal 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
|
||||||
|
}
|
||||||
16
internal/acl/types/context.go
Normal file
16
internal/acl/types/context.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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
44
internal/api/v1/events.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"))
|
|
||||||
}
|
|
||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(¬if.LogMessage{
|
notif.Notify(¬if.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(¬if.LogMessage{
|
notif.Notify(¬if.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()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
10
internal/autocert/types/cert_info.go
Normal file
10
internal/autocert/types/cert_info.go
Normal 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
|
||||||
16
internal/autocert/types/context.go
Normal file
16
internal/autocert/types/context.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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(¬if.LogMessage{
|
notif.Notify(¬if.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(¬if.LogMessage{
|
notif.Notify(¬if.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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
|||||||
51
internal/entrypoint/http_pool_adapter.go
Normal file
51
internal/entrypoint/http_pool_adapter.go
Normal 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
|
||||||
|
}
|
||||||
175
internal/entrypoint/http_server.go
Normal file
175
internal/entrypoint/http_server.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
internal/entrypoint/query.go
Normal file
92
internal/entrypoint/query.go
Normal 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()
|
||||||
|
}
|
||||||
146
internal/entrypoint/routes.go
Normal file
146
internal/entrypoint/routes.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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{}](),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
18
internal/entrypoint/types/context.go
Normal file
18
internal/entrypoint/types/context.go
Normal 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
|
||||||
|
}
|
||||||
58
internal/entrypoint/types/entrypoint.go
Normal file
58
internal/entrypoint/types/entrypoint.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user