Compare commits

..

15 Commits

Author SHA1 Message Date
yusing
95ffd35585 fix(rules): remove empty segments from splitPipe output
Refactored splitPipe function to use forEachPipePart helper, which filters
out empty segments instead of including them in the result. Updated test
expectation to match the new behavior where empty parts between pipes
are no longer included.
2026-02-24 02:52:19 +08:00
yusing
7b0d846576 fix(rules): prevent appending empty command parts in forEachPipePart and remove redundant calculation in parseDoWithBlocks function 2026-02-24 02:05:18 +08:00
yusing
458c7779d3 fix(tests/rules): correct semantic 2026-02-24 02:01:29 +08:00
yusing
dc6c649f2c fix(tests/rules): update HTTP flow YAML test for correct indentation and syntax
Fixes previous commit:  2a51c2ef52
2026-02-24 01:46:26 +08:00
yusing
3c5c3ecac2 fix(rules): handle empty matcher as unconditional rule
The matcherSignature function now treats empty strings as unconditional rules
that match any request,
returning "(any)" as the signature instead of
rejecting them.

This enables proper dead code detection when an unconditional
terminating rule shadows later rules.

Adds test coverage for detecting dead rules caused by unconditional
terminating rules.
2026-02-24 01:42:40 +08:00
yusing
a94442b001 fix(rules): prevent appending empty parts in splitPipe function 2026-02-24 01:36:54 +08:00
yusing
2a51c2ef52 fix(tests/rules): correct HTTP flow YAML test to use new yaml syntax
This is a test in yaml_test which meant to be testing old YAML syntax instead of new DSL
2026-02-24 01:36:35 +08:00
yusing
6477c35b15 docs(rules): update examples to use block syntax 2026-02-24 01:30:50 +08:00
yusing
5b20bbeb6f refactor(rules): simplify nested block detection by removing @ prefix requirement
Changes the nested block syntax detection from requiring `@`
as the first non-space character on a line to using a line-ending brace heuristic.

The parser now recognizes nested blocks when a line ends with an unquoted `{`,
simplifying the syntax and removing the mandatory `@` prefix while maintaining the same functionality.
2026-02-24 01:30:32 +08:00
yusing
5ba475c489 refactor(api/rules): remove IsResponseRule field from ParsedRule and related logic 2026-02-24 01:07:35 +08:00
yusing
54be056530 refactor(rules): improve termination detection and block parsing logic
Refactors the termination detection in the rules DSL to properly handle if-block and if-else-block commands.

Adds new functions `commandsTerminateInPre`, `commandTerminatesInPre`, and `ifElseBlockTerminatesInPre`
to recursively check if command sequences terminate in the pre-phase.

Also improves the Parse function to try block syntax first with proper error handling and fallback to YAML.

Includes test cases for dead code detection with terminating handlers in conditional blocks.
2026-02-24 01:05:54 +08:00
yusing
08de9086c3 fix(rules): buffer log output before writing to stdout/stderr 2026-02-24 00:12:29 +08:00
yusing
1a17f3943a refactor(rules): change default rule from baseline to fallback behavior
The default rule should runs only when no non-default pre rule matches, instead of running first as a baseline.
This follows the old behavior as before the pr is established.:

- Default rules act as fallback handlers that execute only when no matching non-default rule exists in the pre phase
- IfElseBlockCommand now returns early when a condition matches with a nil Do block, instead of falling through to else blocks
- Add nil check for auth handler to allow requests when no auth is configured
- Fix unterminated environment variable parsing to preserve input

Updates tests to verify the new fallback behavior where special rules suppress default rule execution.
2026-02-24 00:11:03 +08:00
yusing
9bb5c54e7c refactor(rules): defer error logging until after FlushRelease
Split error handling into isUnexpectedError predicate and logFlushError
function. Use rm.AppendError() to collect unexpected errors during rule
execution, then log after FlushRelease completes rather than immediately.
Also updates goutils dependency for AppendError method availability.
2026-02-23 23:09:24 +08:00
yusing
faecbab2cb refactor(rules): introduce block DSL, phase-based execution, and flow validation
- add block syntax parser/scanner with nested @blocks and elif/else support
- restructure rule execution into explicit pre/post phases with phase flags
- classify commands by phase and termination behavior
- enforce flow semantics (default rule handling, dead-rule detection)
- expand HTTP flow coverage with block + YAML parity tests and benches
- refresh rules README/spec and update playground/docs integration
2026-02-23 22:24:15 +08:00
183 changed files with 1742 additions and 8723 deletions

View File

@@ -1 +0,0 @@
**/node_modules

View File

@@ -58,12 +58,8 @@ GODOXY_API_ADDR=127.0.0.1:8888
# Local API listening address (unauthenticated, optional)
# Useful for local development, debugging or automation
# Must bind to loopback only (localhost / 127.0.0.1 / ::1) unless GODOXY_LOCAL_API_ALLOW_NON_LOOPBACK is true
GODOXY_LOCAL_API_ADDR=
# WARNING: exposing local API to either LAN or WAN is dangerous, do not enable this unless you know what you're doing
GODOXY_LOCAL_API_ALLOW_NON_LOOPBACK=false
# Metrics
GODOXY_METRICS_DISABLE_CPU=false
GODOXY_METRICS_DISABLE_MEMORY=false

View File

@@ -47,20 +47,14 @@ jobs:
- name: Build CLI
run: |
make cli=1 NAME=${{ matrix.binary_name }} build
make CLI_BIN_PATH=bin/${{ matrix.binary_name }} build-cli
- name: Check binary
run: |
file bin/${{ matrix.binary_name }}
- name: Upload
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary_name }}
path: bin/${{ matrix.binary_name }}
- name: Upload to release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: bin/${{ matrix.binary_name }}

View File

@@ -1,25 +0,0 @@
name: Docker Image CI (nightly)
on:
push:
branches:
- "*" # matches every branch that doesn't contain a '/'
- "*/*" # matches every branch containing a single '/'
- "**" # matches every branch
- "!dependabot/*"
- "!main" # excludes main
- "!compat" # excludes compat branch
jobs:
build-nightly:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/godoxy
tag: nightly
target: main
build-nightly-agent:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/godoxy-agent
tag: nightly
target: agent

View File

@@ -1,20 +1,24 @@
name: Docker Image CI (compat)
name: Docker Image CI (nightly)
on:
push:
branches:
- "compat" # compat branch
- "*" # matches every branch that doesn't contain a '/'
- "*/*" # matches every branch containing a single '/'
- "**" # matches every branch
- "!dependabot/*"
- "!main" # excludes main
jobs:
build-compat:
build-nightly:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/godoxy
tag: compat
tag: nightly
target: main
build-compat-agent:
build-nightly-agent:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/godoxy-agent
tag: compat
tag: nightly
target: agent

View File

@@ -78,7 +78,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ inputs.image_name }}
tags: |
@@ -86,12 +86,12 @@ jobs:
type=ref,event=tag
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
with:
platforms: ${{ matrix.platform }}
- name: Login to registry
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -99,7 +99,7 @@ jobs:
- name: Build and push by digest
id: build
uses: docker/build-push-action@v7
uses: docker/build-push-action@v6
with:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,4 +1,4 @@
name: Refresh Compat from Main Patch
name: Cherry-pick into Compat
on:
push:
@@ -8,7 +8,7 @@ on:
- ".github/workflows/merge-main-into-compat.yml"
jobs:
refresh-compat:
cherry-pick:
runs-on: ubuntu-latest
permissions:
contents: write
@@ -20,9 +20,20 @@ jobs:
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Refresh compat with single patch commit
- name: Cherry-pick commits from last tag
run: |
./scripts/refresh-compat.sh
git fetch origin compat
git checkout compat
CURRENT_TAG=${{ github.ref_name }}
PREV_TAG=$(git describe --tags --abbrev=0 $CURRENT_TAG^ 2>/dev/null || echo "")
if [ -z "$PREV_TAG" ]; then
echo "No previous tag found. Cherry-picking all commits up to $CURRENT_TAG"
git rev-list --reverse --no-merges $CURRENT_TAG | xargs -r git cherry-pick
else
echo "Cherry-picking commits from $PREV_TAG to $CURRENT_TAG"
git rev-list --reverse --no-merges $PREV_TAG..$CURRENT_TAG | xargs -r git cherry-pick
fi
- name: Push compat
run: |
git push origin compat --force
git push origin compat

3
.gitignore vendored
View File

@@ -39,5 +39,8 @@ CLAUDE.md
!.trunk/configs
# minified files
**/*-min.*
# generated CLI commands
cmd/cli/generated_commands.go

View File

@@ -1,30 +0,0 @@
# AGENTS.md
## Principles
DO NOT run build command.
## Documentation
Update package level `README.md` after making significant changes.
## Go Guidelines
1. Use `golang-best-practices` skill.
2. Use `internal/task/task.go` for lifetime management:
- `task.RootTask()` for background operations
- `parent.Subtask()` for nested tasks
- `OnFinished()` and `OnCancel()` callbacks for cleanup
3. Use `gperr "goutils/errs"` to build pretty nested errors:
- `gperr.Multiline()` for multiple operation attempts
- `gperr.NewBuilder()` to collect errors
- `gperr.NewGroup() + group.Go()` to collect errors of multiple concurrent operations
- `gperr.PrependSubject()` to prepend subject to errors
4. Use `github.com/puzpuzpuz/xsync/v4` for lock-free thread safe maps
5. Use `goutils/synk` to retrieve and put byte buffer
## Testing
- Prefer scoped tests
- Prefer `testify`
- Use `-ldflags="-checklinkname=0"`

View File

@@ -1,5 +1,5 @@
# Stage 1: deps
FROM golang:1.26.2-alpine AS deps
FROM golang:1.26.0-alpine AS deps
HEALTHCHECK NONE
# package version does not matter
@@ -18,9 +18,9 @@ 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 go.mod go.sum ./
# for minify
COPY --from=oven/bun:1-alpine /usr/local/bin/bun /usr/local/bin/bun
COPY --from=oven/bun:1-alpine /usr/local/bin/bunx /usr/local/bin/bunx
# 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
RUN --mount=type=cache,target=/root/.cache/go-build \
@@ -34,7 +34,6 @@ FROM deps AS builder
WORKDIR /src
COPY scripts/minify ./scripts/minify
COPY go.mod go.sum ./
COPY Makefile ./
COPY cmd ./cmd

View File

@@ -6,8 +6,8 @@ export GOOS = linux
REPO_URL ?= https://github.com/yusing/godoxy
WEBUI_DIR ?= $(shell pwd)/../godoxy-webui
DOCS_DIR ?= ${WEBUI_DIR}/wiki
WEBUI_DIR ?= ../godoxy-webui
DOCS_DIR ?= wiki
ifneq ($(BRANCH), compat)
GO_TAGS = sonic
@@ -17,22 +17,15 @@ endif
LDFLAGS = -X github.com/yusing/goutils/version.version=${VERSION} -checklinkname=0
PACKAGE ?= ./cmd
ifeq ($(agent), 1)
NAME = godoxy-agent
PWD = ${shell pwd}/agent
else ifeq ($(socket-proxy), 1)
NAME = godoxy-socket-proxy
PWD = ${shell pwd}/socket-proxy
else ifeq ($(cli), 1)
NAME = godoxy-cli
PWD = ${shell pwd}/cmd/cli
PACKAGE = .
else
NAME = godoxy
PWD = ${shell pwd}
godoxy = 1
endif
ifeq ($(trace), 1)
@@ -65,6 +58,7 @@ endif
BUILD_FLAGS += -tags '$(GO_TAGS)' -ldflags='$(LDFLAGS)'
BIN_PATH := $(shell pwd)/bin/${NAME}
CLI_BIN_PATH ?= $(shell pwd)/bin/godoxy-cli
export NAME
export CGO_ENABLED
@@ -82,11 +76,7 @@ endif
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
POST_BUILD = echo;
ifeq ($(godoxy), 1)
POST_BUILD += $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH};
endif
POST_BUILD = $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH};
ifeq ($(docker), 1)
POST_BUILD += mkdir -p /app && mv ${BIN_PATH} /app/run;
endif
@@ -112,45 +102,44 @@ update-go:
sed -i 's|FROM golang:.*-alpine|FROM golang:${go_ver}-alpine|g' $$file; \
done
for path in ${gomod_paths}; do \
echo "go mod tidy $$path"; \
cd ${PWD}/$$path && go mod tidy; \
done
update-deps:
for path in ${gomod_paths}; do \
echo "go get -u $$path"; \
cd ${PWD}/$$path && go get -u ./... && go mod tidy; \
done
mod-tidy:
for path in ${gomod_paths}; do \
echo "go mod tidy $$path"; \
cd ${PWD}/$$path && go mod tidy; \
done
modernize:
for path in ${gomod_paths}; do \
cd ${PWD}/$$path && go fix ./...; \
done
minify:
minify-js:
@if [ "${agent}" = "1" ]; then \
echo "minify: skipped for agent"; \
echo "minify-js: skipped for agent"; \
elif [ "${socket-proxy}" = "1" ]; then \
echo "minify: skipped for socket-proxy"; \
echo "minify-js: skipped for socket-proxy"; \
else \
bun --bun scripts/minify; \
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:
@if [ "${godoxy}" = "1" ]; then \
make minify; \
elif [ "${cli}" = "1" ]; then \
make gen-cli; \
fi
build: minify-js
mkdir -p $(shell dirname ${BIN_PATH})
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ${PACKAGE}
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
${POST_BUILD}
run: minify
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ${PACKAGE}
run: minify-js
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
dev:
docker compose -f dev.compose.yml $(args)
@@ -194,13 +183,16 @@ gen-swagger:
gen-api-types: gen-swagger
# --disable-throw-on-error
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --add-readonly --route-types \
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
--responses -o ${WEBUI_DIR}/src/lib -n api.ts -p internal/api/v1/docs/swagger.json
.PHONY: gen-cli build-cli update-wiki
gen-cli:
cd cmd/cli && go run ./gen
build-cli: gen-cli
mkdir -p $(shell dirname ${CLI_BIN_PATH})
go build -C cmd/cli -o ${CLI_BIN_PATH} .
.PHONY: gen-cli build-cli update-wiki
update-wiki:
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts

View File

@@ -1,6 +1,11 @@
module github.com/yusing/godoxy/agent
go 1.26.2
go 1.26.0
exclude (
github.com/moby/moby/api v1.53.0 // allow older daemon versions
github.com/moby/moby/client v0.2.2 // allow older daemon versions
)
replace (
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
@@ -15,105 +20,90 @@ replace (
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
require (
github.com/gin-gonic/gin v1.12.0
github.com/bytedance/sonic v1.15.0
github.com/gin-gonic/gin v1.11.0
github.com/gorilla/websocket v1.5.3
github.com/pion/dtls/v3 v3.1.2
github.com/pion/transport/v3 v3.1.1
github.com/rs/zerolog v1.35.1
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1
github.com/yusing/godoxy v0.28.0
github.com/yusing/godoxy v0.26.0
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
github.com/yusing/goutils v0.7.0
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.4.1+incompatible
github.com/docker/go-connections v0.7.0
github.com/docker/go-units v0.5.0
github.com/ebitengine/purego v0.10.0 // indirect
github.com/docker/cli v29.2.1+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.2 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/moby/docker-image-spec v1.3.1
github.com/moby/moby/api v1.52.0
github.com/moby/moby/client v0.2.1
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.52.0 // indirect
github.com/moby/moby/client v0.2.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/puzpuzpuz/xsync/v4 v4.5.0 // indirect
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/shirou/gopsutil/v4 v4.26.1 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.70.0 // indirect
github.com/valyala/fasthttp v1.69.0 // indirect
github.com/yusing/ds v0.4.1 // indirect
github.com/yusing/gointernals v0.2.0 // indirect
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260423020826-a0d979d1386c // indirect
github.com/yusing/goutils/http/websocket v0.0.0-20260423020826-a0d979d1386c // indirect
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260218062549-0b0fa3a059ec // indirect
github.com/yusing/goutils/http/websocket v0.0.0-20260218062549-0b0fa3a059ec // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
golang.org/x/arch v0.26.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
golang.org/x/arch v0.24.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
exclude (
github.com/moby/moby/api v1.53.0
github.com/moby/moby/api v1.54.0
github.com/moby/moby/api v1.54.1
github.com/moby/moby/api v1.54.2
)
exclude (
github.com/moby/moby/client v0.2.2
github.com/moby/moby/client v0.3.0
github.com/moby/moby/client v0.4.0
github.com/moby/moby/client v0.4.1
)

View File

@@ -1,19 +1,19 @@
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -24,40 +24,41 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diskfs/go-diskfs v1.9.1 h1:g/UCTC5jZFomhtH4DyF9fG1eRHGgDIjSd1hSjEErXn0=
github.com/diskfs/go-diskfs v1.9.1/go.mod h1:rW9+4MPN1tbMpQqRZlcM3YQsh3Ucc+Q1k1iIqzzmZcg=
github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8=
github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.4.1+incompatible h1:02RT8QqqwtGRn+6SYypv8IUEbD/ltY6sfKCJIoUcGzk=
github.com/docker/cli v29.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-acme/lego/v4 v4.35.1 h1:xTcEBsENs4Ek5EiNpN3JXy7W5aynf5KhJ6D/HuuhXv4=
github.com/go-acme/lego/v4 v4.35.1/go.mod h1:E+4l5mPPg9dwhTz6JJptNLvtj94E2JDObI50QuFJd3s=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -72,14 +73,15 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -91,14 +93,14 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.9.1 h1:wsQUCdYJ4ZvP7RIRKDLtAtmFQc3kxbrv3QqccO5RWzs=
github.com/gotify/server/v2 v2.9.1/go.mod h1:8scw0hiExomp4rJDrXBwRIcgQm7kv74P4Z4B+iM4l8w=
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
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/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
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/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -109,16 +111,19 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/luthermonson/go-proxmox v0.4.1 h1:1WnUBHzCQEa5goHuzewkApi6LKtQcFB8/tXTtS2D5w8=
github.com/luthermonson/go-proxmox v0.4.1/go.mod h1:U6dAkJ+iiwaeb1g/LMWpWuWN4nmvWeXhmoMuYJMumS4=
github.com/magefile/mage v1.17.2 h1:fyXVu1eadI8Ap1HCCNgEhJ5McIWiYhLR8uol64ZZc40=
github.com/magefile/mage v1.17.2/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/luthermonson/go-proxmox v0.4.0 h1:LKXpG9d64zTaQF79wV0kfOnnSwIcdG39m7sc4ga+XZs=
github.com/luthermonson/go-proxmox v0.4.0/go.mod h1:U6dAkJ+iiwaeb1g/LMWpWuWN4nmvWeXhmoMuYJMumS4=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@@ -130,17 +135,16 @@ github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wi
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
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=
@@ -149,29 +153,31 @@ github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkY
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/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg=
github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.22.0 h1:WyPxYRg/c5xUmxZJbtd0QgysHlLBhRA+MngKdJieHxE=
github.com/samber/slog-common v0.22.0/go.mod h1:d/6OaSlzdkl9PFpfRLgn8FwY1OW6EFmPtBpsHX4MrU0=
github.com/samber/slog-zerolog/v2 v2.9.2 h1:DIFzfzDTxHeRyGlfg/D7b2by7VVzcsBTybRPrzjWF4c=
github.com/samber/slog-zerolog/v2 v2.9.2/go.mod h1:2q6cYK2OcN6YfQE/WyCnUtigc+yYf3ozqGsGmRwZR6I=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
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/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
@@ -196,8 +202,8 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=
github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
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/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
@@ -208,49 +214,50 @@ github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtS
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/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver/v2 v2.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro=
go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
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/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
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/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4=
golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
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/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
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/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
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=

View File

@@ -15,7 +15,7 @@ import (
"strings"
"time"
"encoding/json"
"github.com/bytedance/sonic"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent/common"
@@ -366,7 +366,7 @@ func (cfg *AgentConfig) fetchJSON(ctx context.Context, endpoint string, out any)
return resp.StatusCode, nil
}
err = json.Unmarshal(data, out)
err = sonic.Unmarshal(data, out)
if err != nil {
return 0, err
}

View File

@@ -6,7 +6,7 @@ import (
"strconv"
"time"
"encoding/json"
"github.com/bytedance/sonic"
route "github.com/yusing/godoxy/internal/route/types"
)
@@ -53,7 +53,7 @@ func proxyConfigFromHeaders(h http.Header) (cfg Config, err error) {
return cfg, err
}
err = json.Unmarshal(cfgJSON, &cfg)
err = sonic.Unmarshal(cfgJSON, &cfg)
return cfg, err
}
@@ -67,7 +67,7 @@ func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header) {
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header) {
h.Set(HeaderXProxyHost, cfg.Host)
h.Set(HeaderXProxyScheme, string(cfg.Scheme))
cfgJSON, _ := json.Marshal(cfg.HTTPConfig)
cfgJSON, _ := sonic.Marshal(cfg.HTTPConfig)
cfgBase64 := base64.StdEncoding.EncodeToString(cfgJSON)
h.Set(HeaderXProxyConfig, cfgBase64)
}

View File

@@ -8,7 +8,7 @@ import (
"strings"
"time"
"encoding/json"
"github.com/bytedance/sonic"
healthcheck "github.com/yusing/godoxy/internal/health/check"
"github.com/yusing/godoxy/internal/types"
)
@@ -73,7 +73,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(result)
sonic.ConfigDefault.NewEncoder(w).Encode(result)
}
func parseMsOrDefault(msStr string) time.Duration {

View File

@@ -3,7 +3,7 @@ package handler
import (
"net/http"
"encoding/json"
"github.com/bytedance/sonic"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/yusing/godoxy/agent/pkg/agent"
@@ -51,7 +51,7 @@ func NewAgentHandler() http.Handler {
Runtime: env.Runtime,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(agentInfo)
sonic.ConfigDefault.NewEncoder(w).Encode(agentInfo)
})
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
module github.com/yusing/godoxy/cli
go 1.26.2
go 1.26.0
require (
github.com/gorilla/websocket v1.5.3

View File

@@ -21,7 +21,6 @@ import (
"github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/idlewatcher"
idlewatcherTypes "github.com/yusing/godoxy/internal/idlewatcher/types"
"github.com/yusing/goutils/http/reverseproxy"
)
type debugMux struct {
@@ -134,9 +133,6 @@ func listenDebugServer() {
})
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
mux.HandleFunc("Origin unreachable page", "GET", "/reverseproxy/origin_unreachable", func(w http.ResponseWriter, r *http.Request) {
reverseproxy.WriteDebugOriginUnreachablePage(w)
})
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
apiHandler := newAPIHandler(mux)
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
@@ -144,9 +140,8 @@ func listenDebugServer() {
mux.Finalize()
go func() {
log.Info().Msgf("debug server started at http://localhost:7778/")
//nolint:gosec
err := http.ListenAndServe(":7778", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Expires", "0")

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=

View File

@@ -19,17 +19,6 @@
# 3. other providers, see https://docs.godoxy.dev/DNS-01-Providers
# Inbound mTLS profiles (optional)
#
# Reusable named profiles for inbound HTTPS client-certificate validation.
# A profile must trust either the system CA store, one or more CA files, or both.
#
# inbound_mtls_profiles:
# corp:
# use_system_cas: true
# ca_files:
# - /app/certs/corp-ca.pem
# Access Control
# When enabled, it will be applied globally at connection level,
# all incoming connections (web, tcp and udp) will be checked against the ACL rules.
@@ -76,6 +65,7 @@ entrypoint:
Access-Control-Allow-Headers: "*"
Access-Control-Allow-Origin: "*"
Access-Control-Max-Age: 180
Vary: "*"
X-XSS-Protection: 1; mode=block
Content-Security-Policy: "object-src 'self'; frame-ancestors 'self';"
X-Content-Type-Options: nosniff
@@ -156,11 +146,6 @@ providers:
# secret: aaaa-bbbb-cccc-dddd
# no_tls_verify: true
# To relay the downstream client address to a TCP upstream, set
# `relay_proxy_protocol_header: true` on that specific TCP route in route
# configuration (for example, see providers.example.yml). UDP relay is not
# supported yet.
# Match domains
# See https://docs.godoxy.dev/Certificates-and-domain-matching
#

View File

@@ -1,46 +0,0 @@
services:
netbird-dashboard:
image: netbirdio/dashboard:latest
container_name: netbird-dashboard
restart: unless-stopped
networks: [netbird-net]
env_file: dashboard.env
labels:
proxy.aliases: netbird
proxy.#1.port: 80
proxy.#1.scheme: http
proxy.#1.homepage.name: NetBird
proxy.#1.homepage.icon: "@selfhst/netbird.svg"
proxy.#1.homepage.category: networking
proxy.#1.rules: | # https://docs.netbird.io/selfhosted/configuration-files
path glob(/signalexchange.SignalExchange/**) | path glob(/management.ManagementService/**) | path glob(/management.ProxyService/**) {
route netbird-grpc
}
path glob(/relay*) | path glob(/ws-proxy/**) | path glob(/api*) | path glob(/oauth2*) {
route netbird-api
}
default {
pass
}
netbird-server:
image: netbirdio/netbird-server:latest
container_name: netbird-server
restart: unless-stopped
networks:
- netbird-net
volumes:
- netbird_data:/var/lib/netbird
- ./config.yaml:/etc/netbird/config.yaml
command: ["--config", "/etc/netbird/config.yaml"]
labels:
proxy.aliases: netbird-api, netbird-grpc
proxy.*.port: 80
proxy.*.homepage.show: false
proxy.#1.scheme: http
proxy.#2.scheme: h2c
networks:
netbird-net:
volumes:
netbird_data:

174
go.mod
View File

@@ -1,6 +1,11 @@
module github.com/yusing/godoxy
go 1.26.2
go 1.26.0
exclude (
github.com/moby/moby/api v1.53.0 // allow older daemon versions
github.com/moby/moby/client v0.2.2 // allow older daemon versions
)
replace (
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
@@ -15,73 +20,78 @@ replace (
)
require (
github.com/PuerkitoBio/goquery v1.12.0 // parsing HTML for extract fav icon; modify_html middleware
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
github.com/cenkalti/backoff/v5 v5.0.3 // backoff for retrying operations
github.com/coreos/go-oidc/v3 v3.18.0 // oidc authentication
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
github.com/fsnotify/fsnotify v1.9.0 // file watcher
github.com/gin-gonic/gin v1.12.0 // api server
github.com/go-acme/lego/v4 v4.35.1 // acme client
github.com/go-playground/validator/v10 v10.30.2 // validator
github.com/gin-gonic/gin v1.11.0 // api server
github.com/go-acme/lego/v4 v4.32.0 // acme client
github.com/go-playground/validator/v10 v10.30.1 // validator
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
github.com/gotify/server/v2 v2.9.1 // 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/pires/go-proxyproto v0.12.0 // proxy protocol support
github.com/puzpuzpuz/xsync/v4 v4.5.0 // lock free map for concurrent operations
github.com/rs/zerolog v1.35.1 // logging
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/rs/zerolog v1.34.0 // logging
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
golang.org/x/crypto v0.50.0 // encrypting password with bcrypt
golang.org/x/net v0.53.0 // HTTP header utilities
golang.org/x/oauth2 v0.36.0 // oauth2 authentication
golang.org/x/sync v0.20.0 // errgroup and singleflight for concurrent operations
golang.org/x/time v0.15.0 // time utilities
golang.org/x/crypto v0.48.0 // encrypting password with bcrypt
golang.org/x/net v0.50.0 // HTTP header utilities
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/time v0.14.0 // time utilities
)
require (
github.com/bytedance/gopkg v0.1.4 // xxhash64 for fast hash
github.com/bytedance/sonic v1.15.0 // indirect; fast json parsing
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
github.com/bytedance/sonic v1.15.0 // fast json parsing
github.com/docker/cli v29.2.1+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.3.1 // jwt authentication
github.com/luthermonson/go-proxmox v0.4.1 // proxmox API client
github.com/luthermonson/go-proxmox v0.4.0 // proxmox API client
github.com/moby/moby/api v1.52.0 // docker API
github.com/moby/moby/client v0.2.1 // docker client
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
github.com/quic-go/quic-go v0.59.0 // http3 support
github.com/shirou/gopsutil/v4 v4.26.3 // 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/stretchr/testify v1.11.1 // testing framework
github.com/valyala/fasthttp v1.70.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/godoxy/agent v0.0.0-20260423092340-5699f004c807
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260423092340-5699f004c807
github.com/yusing/godoxy/agent v0.0.0-20260218101334-add7884a365e
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260218101334-add7884a365e
github.com/yusing/gointernals v0.2.0
github.com/yusing/goutils v0.7.0
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260423020826-a0d979d1386c
github.com/yusing/goutils/http/websocket v0.0.0-20260423020826-a0d979d1386c
github.com/yusing/goutils/server v0.0.0-20260423020826-a0d979d1386c
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260218062549-0b0fa3a059ec
github.com/yusing/goutils/http/websocket v0.0.0-20260218062549-0b0fa3a059ec
github.com/yusing/goutils/server v0.0.0-20260218062549-0b0fa3a059ec
)
require (
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/diskfs/go-diskfs v1.9.1 // indirect
github.com/diskfs/go-diskfs v1.7.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/docker/go-connections v0.6.0
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // 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/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
@@ -89,51 +99,52 @@ require (
github.com/gofrs/flock v0.13.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magefile/mage v1.17.2 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/samber/lo v1.53.0 // indirect
github.com/samber/slog-common v0.22.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.2 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/samber/slog-common v0.20.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.1 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/atomic v1.11.0
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/api v0.276.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect
google.golang.org/grpc v1.80.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/api v0.267.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@@ -141,48 +152,31 @@ require (
)
require (
github.com/akamai/AkamaiOPEN-edgegrid-golang/v13 v13.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/bodgit/tsig v1.2.2 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/docker/cli v29.4.1+incompatible
github.com/docker/go-connections v0.7.0
github.com/docker/go-units v0.5.0
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect
github.com/gin-contrib/sse v1.1.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-resty/resty/v2 v2.17.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/linode/linodego v1.68.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
github.com/moby/docker-image-spec v1.3.1
github.com/moby/moby/api v1.52.0
github.com/moby/moby/client v0.2.1
github.com/linode/linodego v1.65.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
github.com/nrdcg/goinwx v0.12.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.113.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.113.0 // indirect
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pion/dtls/v3 v3.1.2 // indirect
github.com/pion/logging v0.2.4 // indirect
@@ -194,23 +188,9 @@ require (
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vultr/govultr/v3 v3.31.1 // indirect
github.com/vultr/govultr/v3 v3.27.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
golang.org/x/arch v0.26.0 // indirect
)
exclude (
github.com/moby/moby/api v1.53.0
github.com/moby/moby/api v1.54.0
github.com/moby/moby/api v1.54.1
github.com/moby/moby/api v1.54.2
)
exclude (
github.com/moby/moby/client v0.2.2
github.com/moby/moby/client v0.3.0
github.com/moby/moby/client v0.4.0
github.com/moby/moby/client v0.4.1
golang.org/x/arch v0.24.0 // indirect
)

325
go.sum
View File

@@ -1,18 +1,18 @@
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
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/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/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
@@ -25,21 +25,18 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.1 h1:edShSHV3DV90+kt+CMaEXEzR9QF7wFrPJxVGz2blMIU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.1/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
github.com/akamai/AkamaiOPEN-edgegrid-golang/v13 v13.1.0 h1:KvfpO2utLmpRq0fbC0UZRzdCERfLGLX1/dcYvG7pP7k=
github.com/akamai/AkamaiOPEN-edgegrid-golang/v13 v13.1.0/go.mod h1:AxGyKKxAxaCNeGadscLgo+gBYEAKhNG6tRR5O0HjV30=
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I=
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=
github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
@@ -47,19 +44,17 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bodgit/tsig v1.2.2 h1:RgxTCr8UFUHyU4D8Ygb2UtXtS4niw4B6XYYBpgCjl0k=
github.com/bodgit/tsig v1.2.2/go.mod h1:rIGNOLZOV/UA03fmCUtEFbpWOrIoaOuETkpaeTvnLF4=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -70,27 +65,27 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diskfs/go-diskfs v1.9.1 h1:g/UCTC5jZFomhtH4DyF9fG1eRHGgDIjSd1hSjEErXn0=
github.com/diskfs/go-diskfs v1.9.1/go.mod h1:rW9+4MPN1tbMpQqRZlcM3YQsh3Ucc+Q1k1iIqzzmZcg=
github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8=
github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.4.1+incompatible h1:02RT8QqqwtGRn+6SYypv8IUEbD/ltY6sfKCJIoUcGzk=
github.com/docker/cli v29.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815/go.mod h1:wYFFK4LYXbX7j+76mOq7aiC/EAw2S22CrzPHqgsisPw=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
@@ -101,16 +96,15 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-acme/lego/v4 v4.35.1 h1:xTcEBsENs4Ek5EiNpN3JXy7W5aynf5KhJ6D/HuuhXv4=
github.com/go-acme/lego/v4 v4.35.1/go.mod h1:E+4l5mPPg9dwhTz6JJptNLvtj94E2JDObI50QuFJd3s=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -126,8 +120,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
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-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
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=
@@ -136,10 +130,11 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@@ -156,60 +151,34 @@ 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
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/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.9.1 h1:wsQUCdYJ4ZvP7RIRKDLtAtmFQc3kxbrv3QqccO5RWzs=
github.com/gotify/server/v2 v2.9.1/go.mod h1:8scw0hiExomp4rJDrXBwRIcgQm7kv74P4Z4B+iM4l8w=
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
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-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.3/go.mod h1:dqRwJGXznQrzw6cWmyo6kH+E7jksEQG/CyVWsJEsJO0=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
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/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
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/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
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/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
@@ -222,21 +191,23 @@ 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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/linode/linodego v1.68.0 h1:lAsXuHm/cwQT3KCbVpMGtRiH8IpQl4hUuBOXpqkuNwo=
github.com/linode/linodego v1.68.0/go.mod h1:X7nmTNq1GmZT4bG6w9aiuVrOnhVxYaywrzxM+buC/qU=
github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
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/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magefile/mage v1.17.2 h1:fyXVu1eadI8Ap1HCCNgEhJ5McIWiYhLR8uol64ZZc40=
github.com/magefile/mage v1.17.2/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -250,31 +221,28 @@ github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wi
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
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/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.113.0 h1:OLlJVGHkTHBCXdTGpNn5ay4DV3gOZrVLxlUM6xBQrIM=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.113.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.113.0 h1:bveOZN6gZZjjEM1T9o1TUm8de3zePyDbfuluMpaRJCE=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.113.0/go.mod h1:Ff6Cxm43tuGJHyltQwD3EXLs7kKpkxzqy6DGQcaiS/0=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc=
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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b h1:it0YPE/evO6/m8t8wxis9KFI2F/aleOKsI6d9uz0cEk=
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b/go.mod h1:tNrEB5k8SI+g5kOlsCmL2ELASfpqEofI0+FLBgBdN08=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/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/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
@@ -283,12 +251,13 @@ 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/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
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/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM=
github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -296,22 +265,23 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg=
github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.22.0 h1:WyPxYRg/c5xUmxZJbtd0QgysHlLBhRA+MngKdJieHxE=
github.com/samber/slog-common v0.22.0/go.mod h1:d/6OaSlzdkl9PFpfRLgn8FwY1OW6EFmPtBpsHX4MrU0=
github.com/samber/slog-zerolog/v2 v2.9.2 h1:DIFzfzDTxHeRyGlfg/D7b2by7VVzcsBTybRPrzjWF4c=
github.com/samber/slog-zerolog/v2 v2.9.2/go.mod h1:2q6cYK2OcN6YfQE/WyCnUtigc+yYf3ozqGsGmRwZR6I=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
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/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
@@ -330,7 +300,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
@@ -347,17 +316,16 @@ github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=
github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
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/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/vultr/govultr/v3 v3.31.1 h1:AoJRZ0WDS1J1otp2wk3OD4aYh4EQxFU+kvyOPdCe4+Y=
github.com/vultr/govultr/v3 v3.31.1/go.mod h1:2zyUw9yADQaGwKnwDesmIOlBNLrm7edsCfWHFJpWKf8=
github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
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/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/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
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/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
@@ -365,104 +333,87 @@ github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtS
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/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver/v2 v2.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro=
go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
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/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
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/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
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/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4=
golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
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-20200114155413-6afb5195e5aa/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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
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/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=
@@ -474,7 +425,6 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
@@ -482,34 +432,31 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
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/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.276.0 h1:nVArUtfLEihtW+b0DdcqRGK1xoEm2+ltAihyztq7MKY=
google.golang.org/api v0.276.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
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/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

Submodule goutils updated: 586f5c382e...3be815cb6e

View File

@@ -69,7 +69,7 @@ Initializes the ACL, starts the logger and notification goroutines.
func (c *Config) IPAllowed(ip net.IP) bool
```
Returns true if the IP is allowed based on configured rules. Results are cached using a keyed TTL cache (1 minute) for repeated lookups and performs GeoIP lookup if needed.
Returns true if the IP is allowed based on configured rules. Performs caching and GeoIP lookup if needed.
```go
func (c *Config) WrapTCP(lis net.Listener) net.Listener
@@ -216,8 +216,7 @@ No metrics are currently exposed.
## Security Considerations
- Loopback and private IPs are always allowed unless explicitly denied
- ACL decisions are cached for 1 minute (TTL) to balance performance and memory usage
- Cache uses least-recently-used (LRU) eviction [or document actual eviction policy]
- Cache TTL is 1 minute to limit memory usage
- Notification channel has a buffer of 100 to prevent blocking
- Failed connections are immediately closed without response
@@ -228,6 +227,7 @@ No metrics are currently exposed.
| Invalid matcher syntax | Validation fails on startup | Fix configuration syntax |
| MaxMind database unavailable | GeoIP lookups return unknown location | Default action applies; cache hit still works |
| Notification provider unavailable | Notification dropped | Error logged, continues operation |
| Cache full | No eviction, uses Go map | No action needed |
## Usage Examples

View File

@@ -1,19 +1,18 @@
package acl
import (
"context"
"fmt"
"math"
"net"
"time"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/maxmind"
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/goutils/cache"
gperr "github.com/yusing/goutils/errs"
aclevents "github.com/yusing/goutils/events/acl"
strutils "github.com/yusing/goutils/strings"
@@ -21,8 +20,6 @@ import (
)
type Config struct {
config
Default string `json:"default" validate:"omitempty,oneof=allow deny"` // default: allow
AllowLocal *bool `json:"allow_local"` // default: true
Allow Matchers `json:"allow"`
@@ -35,6 +32,7 @@ type Config struct {
IncludeAllowed *bool `json:"include_allowed,omitzero"` // default: false
} `json:"notify"`
config
valErr gperr.Error
}
@@ -43,7 +41,7 @@ const defaultNotifyInterval = 1 * time.Minute
type config struct {
defaultAllow bool
allowLocal bool
ipCache cache.CachedContextKeyFunc[*checkCache, string]
ipCache *xsync.Map[string, *checkCache]
// will be nil if Notify.To is empty
// these are per IP, reset every Notify.Interval
@@ -68,9 +66,9 @@ type config struct {
type checkCache struct {
*maxmind.IPInfo
allow bool
reason string
created time.Time
}
type ipLog struct {
@@ -81,6 +79,10 @@ type ipLog struct {
const cacheTTL = 1 * time.Minute
func (c *checkCache) Expired() bool {
return c.created.Add(cacheTTL).Before(time.Now())
}
// TODO: add stats
const (
@@ -118,7 +120,7 @@ func (c *Config) Validate() error {
return c.valErr
}
c.ipCache = cache.NewKeyFunc(c.evaluateIP).WithTTL(cacheTTL).Build()
c.ipCache = xsync.NewMap[string, *checkCache]()
if c.Notify.IncludeAllowed != nil {
c.notifyAllowed = *c.Notify.IncludeAllowed
@@ -169,36 +171,16 @@ func (c *Config) Start(parent task.Parent) error {
return nil
}
func (c *Config) newCheckCache(info *maxmind.IPInfo, allow bool, reason string) *checkCache {
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool, reason string) {
if common.ForceResolveCountry && info.City == nil {
maxmind.LookupCity(info)
}
return &checkCache{
c.ipCache.Store(info.Str, &checkCache{
IPInfo: info,
allow: allow,
reason: reason,
}
}
func (c *Config) evaluateIP(_ context.Context, ipStr string) (*checkCache, error) {
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, fmt.Errorf("invalid IP: %q", ipStr)
}
ipInfo := &maxmind.IPInfo{IP: ip, Str: ipStr}
if index := c.Deny.MatchedIndex(ipInfo); index != -1 {
return c.newCheckCache(ipInfo, false, "blocked by deny rule: "+c.Deny[index].raw), nil
}
if index := c.Allow.MatchedIndex(ipInfo); index != -1 {
return c.newCheckCache(ipInfo, true, "allowed by allow rule: "+c.Allow[index].raw), nil
}
reason := "denied by default"
if c.defaultAllow {
reason = "allowed by default"
}
return c.newCheckCache(ipInfo, c.defaultAllow, reason), nil
created: time.Now(),
})
}
func (c *Config) needLogOrNotify() bool {
@@ -214,16 +196,14 @@ func (c *Config) needNotify() bool {
}
func (c *Config) getCachedCity(ip string) string {
record, err := c.ipCache(context.Background(), ip)
if err != nil {
return "unknown location"
record, ok := c.ipCache.Load(ip)
if ok {
if record.City != nil {
if record.City.Country.IsoCode != "" {
return record.City.Country.IsoCode
}
city := record.IPInfo.City
if city != nil {
if city.Country.IsoCode != "" {
return city.Country.IsoCode
return record.City.Location.TimeZone
}
return city.Location.TimeZone
}
return "unknown location"
}
@@ -308,11 +288,31 @@ func (c *Config) IPAllowed(ip net.IP) bool {
}
ipStr := ip.String()
record, err := c.ipCache(context.Background(), ipStr)
if err != nil {
log.Warn().Err(err).Str("ip", ipStr).Msg("unexpected ACL cache lookup error")
record = c.newCheckCache(&maxmind.IPInfo{IP: ip, Str: ipStr}, c.defaultAllow, "invalid ACL cache lookup")
}
record, ok := c.ipCache.Load(ipStr)
if ok && !record.Expired() {
c.logAndNotify(record.IPInfo, record.allow, record.reason)
return record.allow
}
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
if index := c.Deny.MatchedIndex(ipAndStr); index != -1 {
reason := "blocked by deny rule: " + c.Deny[index].raw
c.logAndNotify(ipAndStr, false, reason)
c.cacheRecord(ipAndStr, false, reason)
return false
}
if index := c.Allow.MatchedIndex(ipAndStr); index != -1 {
reason := "allowed by allow rule: " + c.Allow[index].raw
c.logAndNotify(ipAndStr, true, reason)
c.cacheRecord(ipAndStr, true, reason)
return true
}
reason := "denied by default"
if c.defaultAllow {
reason = "allowed by default"
}
c.logAndNotify(ipAndStr, c.defaultAllow, reason)
c.cacheRecord(ipAndStr, c.defaultAllow, reason)
return c.defaultAllow
}

View File

@@ -1,61 +0,0 @@
package acl
import (
"net"
"testing"
"github.com/stretchr/testify/require"
)
func TestIPAllowedCachesDecision(t *testing.T) {
t.Parallel()
testIP := net.ParseIP("8.8.8.8")
require.NotNil(t, testIP)
t.Run("cached allow survives rule changes", func(t *testing.T) {
t.Parallel()
cfg := &Config{
Default: ACLDeny,
AllowLocal: new(false),
Allow: mustMatchers(t, "ip:8.8.8.8"),
}
require.NoError(t, cfg.Validate())
require.True(t, cfg.IPAllowed(testIP))
cfg.Allow = nil
cfg.Deny = mustMatchers(t, "ip:8.8.8.8")
require.True(t, cfg.IPAllowed(testIP))
})
t.Run("cached deny survives rule changes", func(t *testing.T) {
t.Parallel()
cfg := &Config{
Default: ACLAllow,
AllowLocal: new(false),
Deny: mustMatchers(t, "ip:8.8.8.8"),
}
require.NoError(t, cfg.Validate())
require.False(t, cfg.IPAllowed(testIP))
cfg.Deny = nil
cfg.Allow = mustMatchers(t, "ip:8.8.8.8")
require.False(t, cfg.IPAllowed(testIP))
})
}
func mustMatchers(t *testing.T, rules ...string) Matchers {
t.Helper()
matchers := make(Matchers, len(rules))
for i, rule := range rules {
require.NoError(t, matchers[i].Parse(rule))
}
return matchers
}

View File

@@ -7,7 +7,7 @@ import (
"net/http"
"time"
"encoding/json"
"github.com/bytedance/sonic"
"github.com/gorilla/websocket"
"github.com/valyala/fasthttp"
agentPkg "github.com/yusing/godoxy/agent/pkg/agent"
@@ -63,7 +63,7 @@ func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret Heal
ret.Detail = fmt.Sprintf("HTTP %d %s", status, resp.Body())
return ret, nil
} else {
err = json.Unmarshal(resp.Body(), &ret)
err = sonic.Unmarshal(resp.Body(), &ret)
if err != nil {
return ret, err
}

View File

@@ -1,109 +0,0 @@
package api
import (
"net"
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/auth"
apitypes "github.com/yusing/goutils/apitypes"
)
// CSRFMiddleware implements the Signed Double Submit Cookie pattern.
//
// Safe methods (GET/HEAD/OPTIONS): ensure a signed CSRF cookie exists.
// Unsafe methods (POST/PUT/DELETE/PATCH): require X-CSRF-Token header
// matching the cookie value, with a valid HMAC signature.
func CSRFMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
switch c.Request.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
ensureCSRFCookie(c)
c.Next()
return
}
if allowSameOriginAuthBootstrap(c.Request) {
ensureCSRFCookie(c)
c.Next()
return
}
cookie, err := c.Request.Cookie(auth.CSRFCookieName)
if err != nil {
// No cookie at all — issue one so the frontend can retry.
reissueCSRFCookie(c)
c.JSON(http.StatusForbidden, apitypes.Error("missing CSRF token"))
c.Abort()
return
}
cookieToken := canonicalCSRFToken(cookie.Value)
headerToken := canonicalCSRFToken(c.GetHeader(auth.CSRFHeaderName))
if headerToken == "" || cookieToken != headerToken || !auth.ValidateCSRFToken(cookieToken) {
// Stale or forged token — issue a fresh one so the
// frontend can read the new cookie and retry.
reissueCSRFCookie(c)
c.JSON(http.StatusForbidden, apitypes.Error("invalid CSRF token"))
c.Abort()
return
}
c.Next()
}
}
func ensureCSRFCookie(c *gin.Context) {
if _, err := c.Request.Cookie(auth.CSRFCookieName); err == nil {
return
}
reissueCSRFCookie(c)
}
func reissueCSRFCookie(c *gin.Context) {
token, err := auth.GenerateCSRFToken()
if err != nil {
return
}
auth.SetCSRFCookie(c.Writer, c.Request, token)
}
func allowSameOriginAuthBootstrap(r *http.Request) bool {
if r.Method != http.MethodPost {
return false
}
switch r.URL.Path {
case "/api/v1/auth/login", "/api/v1/auth/callback":
return requestSourceMatchesHost(r)
default:
return false
}
}
func requestSourceMatchesHost(r *http.Request) bool {
for _, header := range []string{"Origin", "Referer"} {
value := r.Header.Get(header)
if value == "" {
continue
}
u, err := url.Parse(value)
if err != nil || u.Host == "" {
return false
}
return normalizeHost(u.Hostname()) == normalizeHost(r.Host)
}
return false
}
func normalizeHost(host string) string {
host = strings.ToLower(host)
if h, _, err := net.SplitHostPort(host); err == nil {
return h
}
return host
}
func canonicalCSRFToken(token string) string {
return strings.Trim(strings.TrimSpace(token), "\"")
}

View File

@@ -1,283 +0,0 @@
package api
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yusing/godoxy/internal/auth"
autocert "github.com/yusing/godoxy/internal/autocert/types"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/goutils/task"
)
func TestAuthCheckIssuesCSRFCookie(t *testing.T) {
handler := newAuthenticatedHandler(t)
req := httptest.NewRequest(http.MethodHead, "/api/v1/auth/check", nil)
req.Host = "app.example.com"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusFound, rec.Code)
csrfCookie := findCookie(rec.Result().Cookies(), auth.CSRFCookieName)
require.NotNil(t, csrfCookie)
assert.NotEmpty(t, csrfCookie.Value)
assert.Empty(t, csrfCookie.Domain)
assert.Equal(t, "/", csrfCookie.Path)
assert.Equal(t, http.SameSiteStrictMode, csrfCookie.SameSite)
}
func TestUserPassCallbackAllowsSameOriginFormPostWithoutCSRFCookie(t *testing.T) {
handler := newAuthenticatedHandler(t)
req := newJSONRequest(t, http.MethodPost, "/api/v1/auth/callback", map[string]string{
"username": common.APIUser,
"password": common.APIPassword,
})
req.Host = "app.example.com"
req.Header.Set("Origin", "https://app.example.com")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
tokenCookie := findCookie(rec.Result().Cookies(), "godoxy_token")
require.NotNil(t, tokenCookie)
assert.NotEmpty(t, tokenCookie.Value)
csrfCookie := findCookie(rec.Result().Cookies(), auth.CSRFCookieName)
require.NotNil(t, csrfCookie)
assert.NotEmpty(t, csrfCookie.Value)
}
func TestUserPassCallbackRejectsCrossOriginPostWithoutCSRFCookie(t *testing.T) {
handler := newAuthenticatedHandler(t)
req := newJSONRequest(t, http.MethodPost, "/api/v1/auth/callback", map[string]string{
"username": common.APIUser,
"password": common.APIPassword,
})
req.Host = "app.example.com"
req.Header.Set("Origin", "https://evil.example.com")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusForbidden, rec.Code)
csrfCookie := findCookie(rec.Result().Cookies(), auth.CSRFCookieName)
require.NotNil(t, csrfCookie)
assert.NotEmpty(t, csrfCookie.Value)
}
func TestUserPassCallbackAcceptsValidCSRFCookie(t *testing.T) {
handler := newAuthenticatedHandler(t)
csrfCookie := issueCSRFCookie(t, handler)
req := newJSONRequest(t, http.MethodPost, "/api/v1/auth/callback", map[string]string{
"username": common.APIUser,
"password": common.APIPassword,
})
req.Host = "app.example.com"
req.AddCookie(csrfCookie)
req.Header.Set(auth.CSRFHeaderName, csrfCookie.Value)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
tokenCookie := findCookie(rec.Result().Cookies(), "godoxy_token")
require.NotNil(t, tokenCookie)
assert.NotEmpty(t, tokenCookie.Value)
}
func TestUnsafeRequestAcceptsQuotedCSRFCookieValue(t *testing.T) {
handler := newAuthenticatedHandler(t)
csrfCookie := issueCSRFCookie(t, handler)
sessionToken := issueSessionToken(t)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/logout", nil)
req.Host = "app.example.com"
req.Header.Set("Cookie", `godoxy_token=`+sessionToken+`; godoxy_csrf="`+csrfCookie.Value+`"`)
req.Header.Set(auth.CSRFHeaderName, csrfCookie.Value)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusFound, rec.Code)
}
func TestLogoutRequiresCSRFCookie(t *testing.T) {
handler := newAuthenticatedHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/logout", nil)
req.Host = "app.example.com"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusForbidden, rec.Code)
}
func TestLoginAllowsSameOriginPostWithoutCSRFCookie(t *testing.T) {
handler := newAuthenticatedHandler(t)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", nil)
req.Host = "app.example.com"
req.Header.Set("Origin", "https://app.example.com")
req.Header.Set("Accept", "text/html")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusFound, rec.Code)
csrfCookie := findCookie(rec.Result().Cookies(), auth.CSRFCookieName)
require.NotNil(t, csrfCookie)
assert.NotEmpty(t, csrfCookie.Value)
}
func TestGetLogoutRouteStillAvailableForFrontend(t *testing.T) {
handler := newAuthenticatedHandler(t)
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/logout", nil)
req.Host = "app.example.com"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusFound, rec.Code)
}
func TestCertRenewRejectsCrossOriginWebSocketRequest(t *testing.T) {
handler := newAuthenticatedHandler(t)
provider := &stubAutocertProvider{}
sessionToken := issueSessionToken(t)
req := httptest.NewRequest(http.MethodGet, "/api/v1/cert/renew", nil)
req.Host = "app.example.com"
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Sec-WebSocket-Version", "13")
req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
req.Header.Set("Origin", "https://evil.example.com")
req.AddCookie(&http.Cookie{Name: "godoxy_token", Value: sessionToken})
req = req.WithContext(context.WithValue(req.Context(), autocert.ContextKey{}, provider))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusForbidden, rec.Code)
assert.Zero(t, provider.forceExpiryCalls)
}
func newAuthenticatedHandler(t *testing.T) *gin.Engine {
t.Helper()
gin.SetMode(gin.TestMode)
prevSecret := common.APIJWTSecret
prevUser := common.APIUser
prevPassword := common.APIPassword
prevDisableAuth := common.DebugDisableAuth
prevIssuerURL := common.OIDCIssuerURL
prevSkipOriginCheck := common.APISkipOriginCheck
common.APIJWTSecret = []byte("0123456789abcdef0123456789abcdef")
common.APIUser = "username"
common.APIPassword = "password"
common.DebugDisableAuth = false
common.OIDCIssuerURL = ""
common.APISkipOriginCheck = false
t.Cleanup(func() {
common.APIJWTSecret = prevSecret
common.APIUser = prevUser
common.APIPassword = prevPassword
common.DebugDisableAuth = prevDisableAuth
common.OIDCIssuerURL = prevIssuerURL
common.APISkipOriginCheck = prevSkipOriginCheck
})
require.NoError(t, auth.Initialize())
return NewHandler(true)
}
func issueCSRFCookie(t *testing.T, handler http.Handler) *http.Cookie {
t.Helper()
req := httptest.NewRequest(http.MethodHead, "/api/v1/auth/check", nil)
req.Host = "app.example.com"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
csrfCookie := findCookie(rec.Result().Cookies(), auth.CSRFCookieName)
require.NotNil(t, csrfCookie)
return csrfCookie
}
func issueSessionToken(t *testing.T) string {
t.Helper()
userpass, ok := auth.GetDefaultAuth().(*auth.UserPassAuth)
require.True(t, ok)
token, err := userpass.NewToken()
require.NoError(t, err)
return token
}
func newJSONRequest(t *testing.T, method, target string, body any) *http.Request {
t.Helper()
encoded, err := json.Marshal(body)
require.NoError(t, err)
req := httptest.NewRequest(method, target, bytes.NewReader(encoded))
req.Header.Set("Content-Type", "application/json")
return req
}
func findCookie(cookies []*http.Cookie, name string) *http.Cookie {
for _, cookie := range cookies {
if cookie.Name == name {
return cookie
}
}
return nil
}
type stubAutocertProvider struct {
forceExpiryCalls int
}
func (p *stubAutocertProvider) GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return nil, nil
}
func (p *stubAutocertProvider) GetCertInfos() ([]autocert.CertInfo, error) {
return nil, nil
}
func (p *stubAutocertProvider) ScheduleRenewalAll(task.Parent) {}
func (p *stubAutocertProvider) ObtainCertAll() error {
return nil
}
func (p *stubAutocertProvider) ForceExpiryAll() bool {
p.forceExpiryCalls++
return true
}
func (p *stubAutocertProvider) WaitRenewalDone(context.Context) bool {
return true
}

View File

@@ -56,11 +56,11 @@ func NewHandler(requireAuth bool) *gin.Engine {
if auth.IsEnabled() && requireAuth {
v1Auth := r.Group("/api/v1/auth")
{
v1Auth.HEAD("/check", CSRFMiddleware(), authApi.Check)
v1Auth.POST("/login", CSRFMiddleware(), authApi.Login)
v1Auth.HEAD("/check", authApi.Check)
v1Auth.POST("/login", authApi.Login)
v1Auth.GET("/callback", authApi.Callback)
v1Auth.POST("/callback", CSRFMiddleware(), authApi.Callback)
v1Auth.POST("/logout", CSRFMiddleware(), authApi.Logout)
v1Auth.POST("/callback", authApi.Callback)
v1Auth.POST("/logout", authApi.Logout)
v1Auth.GET("/logout", authApi.Logout)
}
}
@@ -68,7 +68,6 @@ func NewHandler(requireAuth bool) *gin.Engine {
v1 := r.Group("/api/v1")
if auth.IsEnabled() && requireAuth {
v1.Use(AuthMiddleware())
v1.Use(CSRFMiddleware())
}
if common.APISkipOriginCheck {
v1.Use(SkipOriginCheckMiddleware())

View File

@@ -115,7 +115,7 @@ No dedicated metrics exposed by handlers. Request metrics collected by middlewar
- All endpoints (except `/api/v1/version`) require authentication
- Input validation using Gin binding tags
- File read/write handlers are rooted per file type (`config/` or `config/middlewares/`) to prevent traversal into sibling paths
- Path traversal prevention in file operations
- WebSocket connections use same auth middleware as HTTP
## Failure Modes and Recovery
@@ -195,3 +195,5 @@ func listContainers() ([]Container, error) {
```bash
curl http://localhost:8888/health
```
)

View File

@@ -19,7 +19,6 @@ import (
// @Tags cert,websocket
// @Produce plain
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /cert/renew [get]

View File

@@ -375,12 +375,6 @@
"$ref": "#/definitions/SuccessResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
@@ -5131,7 +5125,10 @@
"$ref": "#/definitions/MockResponse"
},
"rules": {
"type": "string",
"type": "array",
"items": {
"$ref": "#/definitions/routeApi.RawRule"
},
"x-nullable": false,
"x-omitempty": false
}
@@ -5441,12 +5438,6 @@
],
"x-nullable": true
},
"inbound_mtls_profile": {
"description": "HTTP-based routes only: must match a configured inbound_mtls_profiles entry and is ignored when entrypoint.inbound_mtls_profile is set",
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"index": {
"description": "Index file to serve for single-page app mode",
"type": "string",
@@ -5508,12 +5499,6 @@
"x-nullable": false,
"x-omitempty": false
},
"relay_proxy_protocol_header": {
"description": "TCP only: relay PROXY protocol header to the destination",
"type": "boolean",
"x-nullable": false,
"x-omitempty": false
},
"response_header_timeout": {
"type": "integer",
"x-nullable": false,
@@ -6941,6 +6926,28 @@
"x-nullable": false,
"x-omitempty": false
},
"routeApi.RawRule": {
"type": "object",
"properties": {
"do": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"name": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"on": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
}
},
"x-nullable": false,
"x-omitempty": false
},
"routeApi.RoutesByProvider": {
"type": "object",
"additionalProperties": {

View File

@@ -905,7 +905,9 @@ definitions:
mockResponse:
$ref: '#/definitions/MockResponse'
rules:
type: string
items:
$ref: '#/definitions/routeApi.RawRule'
type: array
required:
- rules
type: object
@@ -1052,10 +1054,6 @@ definitions:
allOf:
- $ref: '#/definitions/IdlewatcherConfig'
x-nullable: true
inbound_mtls_profile:
description: 'HTTP-based routes only: must match a configured inbound_mtls_profiles
entry and is ignored when entrypoint.inbound_mtls_profile is set'
type: string
index:
description: Index file to serve for single-page app mode
type: string
@@ -1091,9 +1089,6 @@ definitions:
x-nullable: true
purl:
type: string
relay_proxy_protocol_header:
description: 'TCP only: relay PROXY protocol header to the destination'
type: boolean
response_header_timeout:
type: integer
root:
@@ -1840,12 +1835,12 @@ definitions:
type: string
kernel_version:
type: string
load_avg_5m:
type: string
load_avg_15m:
type: string
load_avg_1m:
type: string
load_avg_5m:
type: string
mem_pct:
type: string
mem_total:
@@ -1863,6 +1858,15 @@ definitions:
uptime:
type: string
type: object
routeApi.RawRule:
properties:
do:
type: string
name:
type: string
"on":
type: string
type: object
routeApi.RoutesByProvider:
additionalProperties:
items:
@@ -2173,10 +2177,6 @@ paths:
description: OK
schema:
$ref: '#/definitions/SuccessResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/ErrorResponse'
"403":
description: Forbidden
schema:

View File

@@ -1,7 +1,6 @@
package fileapi
import (
"io"
"net/http"
"os"
"path"
@@ -45,14 +44,7 @@ func Get(c *gin.Context) {
return
}
f, err := request.FileType.OpenFile(request.Filename, os.O_RDONLY, 0)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to open root"))
return
}
defer f.Close()
content, err := io.ReadAll(f)
content, err := os.ReadFile(request.FileType.GetPath(request.Filename))
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to read file"))
return
@@ -73,18 +65,9 @@ func GetFileType(file string) FileType {
return FileTypeProvider
}
func (t FileType) RootPath() string {
func (t FileType) GetPath(filename string) string {
if t == FileTypeMiddleware {
return common.MiddlewareComposeBasePath
return path.Join(common.MiddlewareComposeBasePath, filename)
}
return common.ConfigBasePath
}
func (t FileType) OpenFile(filename string, flag int, perm os.FileMode) (*os.File, error) {
root, err := os.OpenRoot(t.RootPath())
if err != nil {
return nil, err
}
defer root.Close()
return root.OpenFile(filename, flag, perm)
return path.Join(common.ConfigBasePath, filename)
}

View File

@@ -1,101 +0,0 @@
package fileapi_test
import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
api "github.com/yusing/godoxy/internal/api"
fileapi "github.com/yusing/godoxy/internal/api/v1/file"
)
func setupFileAPITestRoot(t *testing.T) string {
t.Helper()
oldWD, err := os.Getwd()
require.NoError(t, err)
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "config", "middlewares"), 0o755))
require.NoError(t, os.Chdir(root))
t.Cleanup(func() {
require.NoError(t, os.Chdir(oldWD))
})
return root
}
func newFileContentRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(api.ErrorHandler())
r.GET("/api/v1/file/content", fileapi.Get)
r.PUT("/api/v1/file/content", fileapi.Set)
return r
}
func TestGet_PathTraversalBlocked(t *testing.T) {
root := setupFileAPITestRoot(t)
const (
insideFilename = "providers.yml"
insideContent = "app: inside\n"
outsideContent = "app: outside\n"
)
require.NoError(t, os.WriteFile(filepath.Join(root, "config", insideFilename), []byte(insideContent), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(root, "secret.yml"), []byte(outsideContent), 0o644))
r := newFileContentRouter()
t.Run("read_in_root_file", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/file/content?type=config&filename="+insideFilename, nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, insideContent, w.Body.String())
})
tests := []struct {
name string
filename string
queryEscaped bool
}{
{
name: "dotdot_traversal_to_sibling_file",
filename: "../secret.yml",
},
{
name: "url_encoded_dotdot_traversal_to_sibling_file",
filename: "../secret.yml",
queryEscaped: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filename := tt.filename
if tt.queryEscaped {
filename = url.QueryEscape(filename)
}
url := "/api/v1/file/content?type=config&filename=" + filename
req := httptest.NewRequest(http.MethodGet, url, nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// "Blocked" means we should never successfully read the outside file.
assert.NotEqual(t, http.StatusOK, w.Code)
assert.NotEqual(t, outsideContent, w.Body.String())
})
}
}

View File

@@ -43,14 +43,7 @@ func Set(c *gin.Context) {
return
}
f, err := request.FileType.OpenFile(request.Filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to open file"))
return
}
defer f.Close()
_, err = f.Write(content)
err = os.WriteFile(request.FileType.GetPath(request.Filename), content, 0o644)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to write file"))
return

View File

@@ -1,87 +0,0 @@
package fileapi_test
import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const validProviderYAML = `app:
host: attacker.com
port: 443
scheme: https
`
func TestSet_PathTraversalBlocked(t *testing.T) {
root := setupFileAPITestRoot(t)
r := newFileContentRouter()
t.Run("write_in_root_file", func(t *testing.T) {
req := httptest.NewRequest(
http.MethodPut,
"/api/v1/file/content?type=provider&filename=providers.yml",
strings.NewReader(validProviderYAML),
)
req.Header.Set("Content-Type", "text/plain")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
content, err := os.ReadFile(filepath.Join(root, "config", "providers.yml"))
require.NoError(t, err)
assert.Equal(t, validProviderYAML, string(content))
})
const originalContent = "do not overwrite\n"
require.NoError(t, os.WriteFile(filepath.Join(root, "secret.yml"), []byte(originalContent), 0o644))
tests := []struct {
name string
filename string
queryEscaped bool
}{
{
name: "dotdot_traversal_to_sibling_file",
filename: "../secret.yml",
},
{
name: "url_encoded_dotdot_traversal_to_sibling_file",
filename: "../secret.yml",
queryEscaped: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filename := tt.filename
if tt.queryEscaped {
filename = url.QueryEscape(filename)
}
req := httptest.NewRequest(
http.MethodPut,
"/api/v1/file/content?type=provider&filename="+filename,
strings.NewReader(validProviderYAML),
)
req.Header.Set("Content-Type", "text/plain")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.NotEqual(t, http.StatusOK, w.Code)
content, err := os.ReadFile(filepath.Join(root, "secret.yml"))
require.NoError(t, err)
assert.Equal(t, originalContent, string(content))
})
}
}

View File

@@ -8,6 +8,7 @@ import (
"sync/atomic"
"time"
"github.com/bytedance/sonic"
"github.com/cenkalti/backoff/v5"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
@@ -240,7 +241,7 @@ func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any)
defer bufFromPool.release(bufFromPool.RawMessage)
}
err := json.NewEncoder(buf).Encode(map[string]any{
err := sonic.ConfigDefault.NewEncoder(buf).Encode(map[string]any{
agentName: systemInfo,
})
if err != nil {

View File

@@ -9,7 +9,6 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/goccy/go-yaml"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/route/rules"
apitypes "github.com/yusing/goutils/apitypes"
@@ -24,7 +23,7 @@ type RawRule struct {
}
type PlaygroundRequest struct {
Rules string `json:"rules" binding:"required"`
Rules []RawRule `json:"rules" binding:"required"`
MockRequest MockRequest `json:"mockRequest"`
MockResponse MockResponse `json:"mockResponse"`
} // @name PlaygroundRequest
@@ -256,35 +255,7 @@ func handlerWithRecover(w http.ResponseWriter, r *http.Request, h http.HandlerFu
h(w, r)
}
func parseRules(config string) ([]ParsedRule, rules.Rules, error) {
config = strings.TrimSpace(config)
if config == "" {
return []ParsedRule{}, nil, nil
}
var rawRules []RawRule
if err := yaml.Unmarshal([]byte(config), &rawRules); err == nil && len(rawRules) > 0 {
return parseRawRules(rawRules)
}
var rulesList rules.Rules
if err := rulesList.Parse(config); err != nil {
return nil, nil, err
}
parsedRules := make([]ParsedRule, 0, len(rulesList))
for _, rule := range rulesList {
parsedRules = append(parsedRules, ParsedRule{
Name: rule.Name,
On: rule.On.String(),
Do: rule.Do.String(),
})
}
return parsedRules, rulesList, nil
}
func parseRawRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
parsedRules := make([]ParsedRule, 0, len(rawRules))
rulesList := make(rules.Rules, 0, len(rawRules))

View File

@@ -22,10 +22,13 @@ func TestPlayground(t *testing.T) {
{
name: "simple path matching rule",
request: PlaygroundRequest{
Rules: `- name: test rule
on: path /api
do: pass
`,
Rules: []RawRule{
{
Name: "test rule",
On: "path /api",
Do: "pass",
},
},
MockRequest: MockRequest{
Method: "GET",
Path: "/api",
@@ -50,10 +53,13 @@ func TestPlayground(t *testing.T) {
{
name: "header matching rule",
request: PlaygroundRequest{
Rules: `- name: check user agent
on: header User-Agent Chrome
do: error 403 Forbidden
`,
Rules: []RawRule{
{
Name: "check user agent",
On: "header User-Agent Chrome",
Do: "error 403 Forbidden",
},
},
MockRequest: MockRequest{
Method: "GET",
Path: "/",
@@ -84,10 +90,13 @@ func TestPlayground(t *testing.T) {
{
name: "invalid rule syntax",
request: PlaygroundRequest{
Rules: `- name: bad rule
on: invalid_checker something
do: pass
`,
Rules: []RawRule{
{
Name: "bad rule",
On: "invalid_checker something",
Do: "pass",
},
},
MockRequest: MockRequest{
Method: "GET",
Path: "/",
@@ -106,10 +115,13 @@ func TestPlayground(t *testing.T) {
{
name: "rewrite path rule",
request: PlaygroundRequest{
Rules: `- name: rewrite rule
on: path glob(/api/*)
do: rewrite /api/ /v1/
`,
Rules: []RawRule{
{
Name: "rewrite rule",
On: "path glob(/api/*)",
Do: "rewrite /api/ /v1/",
},
},
MockRequest: MockRequest{
Method: "GET",
Path: "/api/users",
@@ -136,10 +148,13 @@ func TestPlayground(t *testing.T) {
{
name: "method matching rule",
request: PlaygroundRequest{
Rules: `- name: block POST
on: method POST
do: error "405" "Method Not Allowed"
`,
Rules: []RawRule{
{
Name: "block POST",
On: "method POST",
Do: `error "405" "Method Not Allowed"`,
},
},
MockRequest: MockRequest{
Method: "POST",
Path: "/api",
@@ -158,63 +173,6 @@ func TestPlayground(t *testing.T) {
}
},
},
{
name: "block syntax default rule",
request: PlaygroundRequest{
Rules: `default {
pass
}`,
MockRequest: MockRequest{
Method: "GET",
Path: "/",
},
},
wantStatusCode: http.StatusOK,
checkResponse: func(t *testing.T, resp PlaygroundResponse) {
if len(resp.ParsedRules) != 1 {
t.Errorf("expected 1 parsed rule, got %d", len(resp.ParsedRules))
}
if resp.ParsedRules[0].ValidationError != nil {
t.Errorf("expected rule to be valid, got error: %v", resp.ParsedRules[0].ValidationError)
}
if !resp.UpstreamCalled {
t.Error("expected upstream to be called")
}
},
},
{
name: "block syntax conditional rule",
request: PlaygroundRequest{
Rules: `header User-Agent Chrome {
error 403 Forbidden
}`,
MockRequest: MockRequest{
Method: "GET",
Path: "/",
Headers: map[string][]string{
"User-Agent": {"Chrome"},
},
},
},
wantStatusCode: http.StatusOK,
checkResponse: func(t *testing.T, resp PlaygroundResponse) {
if len(resp.ParsedRules) != 1 {
t.Errorf("expected 1 parsed rule, got %d", len(resp.ParsedRules))
}
if resp.ParsedRules[0].ValidationError != nil {
t.Errorf("expected rule to be valid, got error: %v", resp.ParsedRules[0].ValidationError)
}
if len(resp.MatchedRules) != 1 {
t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules))
}
if resp.FinalResponse.StatusCode != http.StatusForbidden {
t.Errorf("expected status 403, got %d", resp.FinalResponse.StatusCode)
}
if resp.UpstreamCalled {
t.Error("expected upstream not to be called")
}
},
},
}
for _, tt := range tests {

View File

@@ -7,7 +7,7 @@ import (
_ "embed"
)
//go:embed block_page.min.html
//go:embed block_page.html
var blockPageHTML string
var blockPageTemplate = template.Must(template.New("block_page").Parse(blockPageHTML))

View File

@@ -1,225 +0,0 @@
<!doctype html>
<html lang="en">
<meta charset="UTF-8" /><meta
name="viewport"
content="width=device-width,initial-scale=1.0"
/><title>Access Denied</title
><meta name="color-scheme" content="dark" /><style>
:root {
color-scheme: dark;
--bg0: #070a12;
--bg1: #0b1020;
--card: #ffffff0e;
--card2: #ffffff0d;
--text: #ffffffeb;
--muted: #ffffffad;
--border: #ffffff1f;
--borderSoft: #ffffff14;
--borderStrong: #ffffff24;
--borderHover: #ffffff38;
--shadow: 0 22px 60px #0000008c;
--shadowCard: 0 22px 60px #00000094;
--shadowButton: 0 12px 28px #00000059;
--insetHighlight: inset 0 1px 0 #ffffff0a;
--ring: #78a0d26b;
--accent0: #7aa3c8;
--accent1: #9a8bc7;
--btn: #ffffff0f;
--btnHover: #ffffff14;
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
body {
color: var(--text);
background-color: var(--bg1);
background-image: none;
margin: 0;
font-family:
ui-sans-serif,
system-ui,
-apple-system,
Segoe UI,
Roboto,
Helvetica,
Arial,
Apple Color Emoji,
Segoe UI Emoji;
}
.wrap {
place-items: center;
min-height: 100%;
padding: 28px 16px;
display: grid;
}
.card {
background: var(--card);
border: 1px solid var(--border);
width: min(720px, 100%);
box-shadow: var(--shadowCard), var(--insetHighlight);
border-radius: 16px;
overflow: hidden;
}
.topbar {
border-bottom: 1px solid var(--borderSoft);
background: var(--card2);
align-items: center;
gap: 12px;
padding: 18px 18px 12px;
display: flex;
}
.badge {
border: 1px solid var(--borderStrong);
background: var(--card2);
border-radius: 12px;
place-items: center;
width: 38px;
height: 38px;
display: grid;
}
.badge svg {
opacity: 0.95;
}
.badge .bang {
color: #ffffffe6;
font-size: 22px;
font-weight: 700;
line-height: 1;
transform: translateY(-1px);
}
h1 {
letter-spacing: 0.2px;
margin: 0;
font-size: 18px;
line-height: 1.25;
}
.sub {
color: var(--muted);
margin: 2px 0 0;
font-size: 13px;
}
.content {
padding: 18px;
}
.error {
color: #fffc;
white-space: pre-wrap;
word-break: break-word;
text-transform: capitalize;
background: #00000040;
border: 1px solid #ffffff1a;
border-radius: 12px;
margin: 0;
padding: 14px;
font-family:
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
Liberation Mono,
Courier New,
monospace;
font-size: 13px;
line-height: 1.55;
}
.actions {
flex-wrap: wrap;
align-items: center;
gap: 10px;
margin-top: 14px;
display: flex;
}
a.button {
color: #ffffffeb;
border: 1px solid var(--borderStrong);
background: var(--btn);
box-shadow: var(--shadowButton);
border-radius: 10px;
justify-content: center;
align-items: center;
gap: 8px;
padding: 8px 12px;
font-size: 14px;
text-decoration: none;
transition:
transform 0.12s,
border-color 0.12s,
background 0.12s,
box-shadow 0.12s;
display: inline-flex;
}
a.button:hover {
border-color: var(--borderHover);
background: var(--btnHover);
transform: translateY(-1px);
}
a.button:focus-visible {
box-shadow:
0 0 0 3px var(--ring),
var(--shadowButton);
outline: 0;
}
.hint {
color: var(--muted);
font-size: 12px;
line-height: 1.4;
}
.hint kbd {
border: 1px solid var(--borderStrong);
background: var(--btn);
color: #ffffffdb;
border-radius: 6px;
padding: 2px 4px;
font-family:
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
Liberation Mono,
Courier New,
monospace;
font-size: 11px;
}
kbd {
font-weight: 500;
}
.kbd-container {
align-items: center;
gap: 2px;
display: inline-flex;
}
</style>
<div class="wrap">
<main class="card" role="main" aria-labelledby="title">
<header class="topbar">
<div class="badge" aria-hidden="true"><span class="bang">!</span></div>
<div>
<h1 id="title">{{.StatusText}}</h1>
<p class="sub">You dont have permission to access this resource.</p>
</div>
</header>
<section class="content">
<pre class="error">{{.Error}}</pre>
<div class="actions">
<a class="button" href="{{.ActionURL}}"
><span>{{.ActionText}}</span><span aria-hidden="true"></span></a
>
<div class="hint">
If you just signed in, try refreshing the page.<span
aria-hidden="true"
></span>
<div class="kbd-container">
<kbd>Ctrl</kbd><span>+</span><kbd>R</kbd>
</div>
</div>
</div>
</section>
</main>
</div>
</html>

View File

@@ -1,84 +0,0 @@
package auth
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"net/http"
"strings"
"github.com/yusing/godoxy/internal/common"
"golang.org/x/crypto/hkdf"
)
const (
CSRFCookieName = "godoxy_csrf"
CSRFHKDFSalt = "godoxy-csrf"
CSRFHeaderName = "X-CSRF-Token"
csrfTokenLength = 32
)
// csrfSecret is derived from API_JWT_SECRET via HKDF for cryptographic
// separation from JWT signing. Falls back to an ephemeral random key
// for OIDC-only setups where no JWT secret is configured.
var csrfSecret = func() []byte {
if common.APIJWTSecret != nil {
return hkdf.Extract(sha256.New, common.APIJWTSecret, []byte(CSRFHKDFSalt))
}
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
panic("failed to generate CSRF secret: " + err.Error())
}
return b
}()
func GenerateCSRFToken() (string, error) {
nonce := make([]byte, csrfTokenLength)
if _, err := rand.Read(nonce); err != nil {
return "", err
}
nonceHex := hex.EncodeToString(nonce)
return nonceHex + "." + csrfSign(nonceHex), nil
}
// ValidateCSRFToken checks the HMAC signature embedded in the token.
// This prevents subdomain cookie-injection attacks where an attacker
// sets a forged CSRF cookie — they cannot produce a valid signature
// without the ephemeral secret.
func ValidateCSRFToken(token string) bool {
nonce, sig, ok := strings.Cut(token, ".")
if !ok || len(nonce) != csrfTokenLength*2 {
return false
}
return hmac.Equal([]byte(sig), []byte(csrfSign(nonce)))
}
func csrfSign(nonce string) string {
mac := hmac.New(sha256.New, csrfSecret)
mac.Write([]byte(nonce))
return hex.EncodeToString(mac.Sum(nil))
}
func SetCSRFCookie(w http.ResponseWriter, r *http.Request, token string) {
http.SetCookie(w, &http.Cookie{
Name: CSRFCookieName,
Value: token,
HttpOnly: false,
Secure: common.APIJWTSecure,
SameSite: http.SameSiteStrictMode,
Path: "/",
})
}
func ClearCSRFCookie(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: CSRFCookieName,
Value: "",
MaxAge: -1,
HttpOnly: false,
Secure: common.APIJWTSecure,
SameSite: http.SameSiteStrictMode,
Path: "/",
})
}

View File

@@ -216,7 +216,7 @@ func TestOIDCCallbackHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/auth/callback?code="+tt.code+"&state="+tt.state, nil)
if tt.state != "" {
req.AddCookie(&http.Cookie{
Name: defaultAuth.(*OIDCProvider).getAppScopedCookieName(CookieOauthState),
Name: CookieOauthState,
Value: tt.state,
})
}

View File

@@ -6,7 +6,7 @@ import (
"net/http"
"time"
"encoding/json"
"github.com/bytedance/sonic"
"github.com/golang-jwt/jwt/v5"
"github.com/yusing/godoxy/internal/common"
httputils "github.com/yusing/goutils/http"
@@ -107,7 +107,7 @@ type UserPassAuthCallbackRequest struct {
func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
var creds UserPassAuthCallbackRequest
err := json.NewDecoder(r.Body).Decode(&creds)
err := sonic.ConfigDefault.NewDecoder(r.Body).Decode(&creds)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return

View File

@@ -151,13 +151,6 @@ flowchart TD
style U fill:#84261A,color:#fff
```
### Concurrency Model
- Certificate obtain / renew work is serialized per provider.
- Extra providers may still renew in parallel with each other.
- Each provider uses its own ACME HTTP client instance so parallel renewals do not mutate shared transport state.
- TLS certificate replacement and SNI matcher rebuilds are synchronized before new state becomes visible to handshakes.
### SNI Matching Flow
```mermaid

View File

@@ -198,7 +198,7 @@ func (cfg *Config) GetLegoConfig() (*User, *lego.Config, error) {
legoCfg.Certificate.KeyType = certcrypto.EC256
if cfg.HTTPClient != nil {
legoCfg.HTTPClient = cloneHTTPClient(cfg.HTTPClient)
legoCfg.HTTPClient = cfg.HTTPClient
}
if cfg.CADirURL != "" {
@@ -216,18 +216,6 @@ func (cfg *Config) GetLegoConfig() (*User, *lego.Config, error) {
return user, legoCfg, nil
}
func cloneHTTPClient(client *http.Client) *http.Client {
if client == nil {
return nil
}
clone := *client
if transport, ok := client.Transport.(*http.Transport); ok && transport != nil {
clone.Transport = transport.Clone()
}
return &clone
}
func MergeExtraConfig(mainCfg *Config, extraCfg *ConfigExtra) ConfigExtra {
merged := ConfigExtra(*mainCfg)
merged.Extra = nil

View File

@@ -32,9 +32,6 @@ import (
type (
Provider struct {
mu sync.RWMutex
obtain sync.Mutex
logger zerolog.Logger
cfg *Config
@@ -99,36 +96,32 @@ func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) (*Provider, erro
}
func (p *Provider) GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
tlsCert := p.getTLSCert()
if tlsCert == nil {
if p.tlsCert == nil {
return nil, ErrNoCertificates
}
if hello == nil || hello.ServerName == "" {
return tlsCert, nil
return p.tlsCert, nil
}
if prov := p.getSNIMatcher().match(hello.ServerName); prov != nil {
if cert := prov.getTLSCert(); cert != nil {
return cert, nil
if prov := p.sniMatcher.match(hello.ServerName); prov != nil && prov.tlsCert != nil {
return prov.tlsCert, nil
}
}
return tlsCert, nil
return p.tlsCert, nil
}
func (p *Provider) GetCertInfos() ([]autocert.CertInfo, error) {
allProviders := p.allProviders()
certInfos := make([]autocert.CertInfo, 0, len(allProviders))
for _, provider := range allProviders {
tlsCert := provider.getTLSCert()
if tlsCert == nil || tlsCert.Leaf == nil {
if provider.tlsCert == nil {
continue
}
certInfos = append(certInfos, autocert.CertInfo{
Subject: tlsCert.Leaf.Subject.CommonName,
Issuer: tlsCert.Leaf.Issuer.CommonName,
NotBefore: tlsCert.Leaf.NotBefore.Unix(),
NotAfter: tlsCert.Leaf.NotAfter.Unix(),
DNSNames: tlsCert.Leaf.DNSNames,
EmailAddresses: tlsCert.Leaf.EmailAddresses,
Subject: provider.tlsCert.Leaf.Subject.CommonName,
Issuer: provider.tlsCert.Leaf.Issuer.CommonName,
NotBefore: provider.tlsCert.Leaf.NotBefore.Unix(),
NotAfter: provider.tlsCert.Leaf.NotAfter.Unix(),
DNSNames: provider.tlsCert.Leaf.DNSNames,
EmailAddresses: provider.tlsCert.Leaf.EmailAddresses,
})
}
@@ -158,9 +151,7 @@ func (p *Provider) GetKeyPath() string {
}
func (p *Provider) GetExpiries() CertExpiries {
p.mu.RLock()
defer p.mu.RUnlock()
return maps.Clone(p.certExpiries)
return p.certExpiries
}
func (p *Provider) GetLastFailure() (time.Time, error) {
@@ -168,25 +159,17 @@ func (p *Provider) GetLastFailure() (time.Time, error) {
return time.Time{}, nil
}
p.mu.RLock()
lastFailure := p.lastFailure
p.mu.RUnlock()
if lastFailure.IsZero() {
if p.lastFailure.IsZero() {
data, err := os.ReadFile(p.lastFailureFile)
if err != nil {
if !os.IsNotExist(err) {
return time.Time{}, err
}
} else {
parsed, _ := time.Parse(time.RFC3339, string(data))
p.mu.Lock()
p.lastFailure = parsed
lastFailure = p.lastFailure
p.mu.Unlock()
p.lastFailure, _ = time.Parse(time.RFC3339, string(data))
}
}
return lastFailure, nil
return p.lastFailure, nil
}
func (p *Provider) UpdateLastFailure() error {
@@ -194,9 +177,7 @@ func (p *Provider) UpdateLastFailure() error {
return nil
}
t := time.Now()
p.mu.Lock()
p.lastFailure = t
p.mu.Unlock()
return os.WriteFile(p.lastFailureFile, t.AppendFormat(nil, time.RFC3339), 0o600)
}
@@ -204,9 +185,7 @@ func (p *Provider) ClearLastFailure() error {
if common.IsTest {
return nil
}
p.mu.Lock()
p.lastFailure = time.Time{}
p.mu.Unlock()
err := os.Remove(p.lastFailureFile)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
@@ -280,9 +259,6 @@ func (p *Provider) ObtainCertAll() error {
// ObtainCert renews existing certificate or obtains a new certificate for this provider.
func (p *Provider) ObtainCert() error {
p.obtain.Lock()
defer p.obtain.Unlock()
if p.cfg.Provider == ProviderLocal {
return nil
}
@@ -296,19 +272,10 @@ func (p *Provider) ObtainCert() error {
return nil
}
p.mu.RLock()
client := p.client
userRegistered := p.user.Registration != nil
legoCert := p.legoCert
p.mu.RUnlock()
if client == nil {
if p.client == nil {
if err := p.initClient(); err != nil {
return err
}
p.mu.RLock()
client = p.client
p.mu.RUnlock()
}
// mark it as failed first, clear it later if successful
@@ -318,7 +285,7 @@ func (p *Provider) ObtainCert() error {
return fmt.Errorf("failed to update last failure: %w", err)
}
if !userRegistered {
if p.user.Registration == nil {
if err := p.registerACME(); err != nil {
return err
}
@@ -327,24 +294,20 @@ func (p *Provider) ObtainCert() error {
var cert *certificate.Resource
var err error
if legoCert != nil {
cert, err = client.Certificate.RenewWithOptions(*legoCert, &certificate.RenewOptions{
if p.legoCert != nil {
cert, err = p.client.Certificate.RenewWithOptions(*p.legoCert, &certificate.RenewOptions{
Bundle: true,
})
if err != nil {
p.mu.Lock()
p.legoCert = nil
p.mu.Unlock()
log.Err(err).Msg("cert renew failed, fallback to obtain")
} else {
p.mu.Lock()
p.legoCert = cert
p.mu.Unlock()
}
}
if cert == nil {
cert, err = client.Certificate.Obtain(certificate.ObtainRequest{
cert, err = p.client.Certificate.Obtain(certificate.ObtainRequest{
Domains: p.cfg.Domains,
Bundle: true,
})
@@ -366,10 +329,8 @@ func (p *Provider) ObtainCert() error {
if err != nil {
return err
}
p.mu.Lock()
p.tlsCert = &tlsCert
p.certExpiries = expiries
p.mu.Unlock()
p.rebuildSNIMatcher()
if err := p.ClearLastFailure(); err != nil {
@@ -400,10 +361,8 @@ func (p *Provider) loadCert() error {
return err
}
p.mu.Lock()
p.tlsCert = &cert
p.certExpiries = expiries
p.mu.Unlock()
return nil
}
@@ -411,7 +370,7 @@ func (p *Provider) loadCert() error {
// PrintCertExpiriesAll prints the certificate expiries for this provider and all extra providers.
func (p *Provider) PrintCertExpiriesAll() {
for _, provider := range p.allProviders() {
for domain, expiry := range provider.GetExpiries() {
for domain, expiry := range provider.certExpiries {
p.logger.Info().Str("domain", domain).Msgf("certificate expire on %s", strutils.FormatTime(expiry))
}
}
@@ -419,8 +378,6 @@ func (p *Provider) PrintCertExpiriesAll() {
// ShouldRenewOn returns the time at which the certificate should be renewed.
func (p *Provider) ShouldRenewOn() time.Time {
p.mu.RLock()
defer p.mu.RUnlock()
for _, expiry := range p.certExpiries {
return expiry.AddDate(0, -1, 0) // 1 month before
}
@@ -560,22 +517,16 @@ func (p *Provider) initClient() error {
return err
}
p.mu.Lock()
p.client = legoClient
p.mu.Unlock()
return nil
}
func (p *Provider) registerACME() error {
p.mu.RLock()
registrationExists := p.user.Registration != nil
client := p.client
p.mu.RUnlock()
if registrationExists {
if p.user.Registration != nil {
return nil
}
reg, err := client.Registration.ResolveAccountByKey()
reg, err := p.client.Registration.ResolveAccountByKey()
if err == nil {
p.user.Registration = reg
log.Info().Msg("reused acme registration from private key")
@@ -583,20 +534,18 @@ func (p *Provider) registerACME() error {
}
if p.cfg.EABKid != "" && p.cfg.EABHmac != "" {
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
reg, err = p.client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: p.cfg.EABKid,
HmacEncoded: p.cfg.EABHmac,
})
} else {
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
reg, err = p.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
}
if err != nil {
return err
}
p.mu.Lock()
p.user.Registration = reg
p.mu.Unlock()
log.Info().Interface("reg", reg).Msg("acme registered")
return nil
}
@@ -630,9 +579,6 @@ func (p *Provider) saveCert(cert *certificate.Resource) error {
}
func (p *Provider) certState() CertState {
p.mu.RLock()
defer p.mu.RUnlock()
if time.Now().After(p.ShouldRenewOn()) {
return CertStateExpired
}
@@ -720,26 +666,9 @@ func (p *Provider) rebuildSNIMatcher() {
return
}
matcher := sniMatcher{}
matcher.addProvider(p)
p.sniMatcher = sniMatcher{}
p.sniMatcher.addProvider(p)
for _, ep := range p.extraProviders {
matcher.addProvider(ep)
p.sniMatcher.addProvider(ep)
}
p.mu.Lock()
p.sniMatcher = matcher
p.mu.Unlock()
}
func (p *Provider) getSNIMatcher() *sniMatcher {
p.mu.RLock()
defer p.mu.RUnlock()
matcher := p.sniMatcher
return &matcher
}
func (p *Provider) getTLSCert() *tls.Certificate {
p.mu.RLock()
defer p.mu.RUnlock()
return p.tlsCert
}

View File

@@ -62,14 +62,10 @@ func normalizeServerName(s string) string {
}
func (m *sniMatcher) addProvider(p *Provider) {
if p == nil {
if p == nil || p.tlsCert == nil || len(p.tlsCert.Certificate) == 0 {
return
}
tlsCert := p.getTLSCert()
if tlsCert == nil || len(tlsCert.Certificate) == 0 {
return
}
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
leaf, err := x509.ParseCertificate(p.tlsCert.Certificate[0])
if err != nil {
return
}

View File

@@ -36,7 +36,6 @@ var (
LocalAPIHTTPHost,
LocalAPIHTTPPort,
LocalAPIHTTPURL = env.GetAddrEnv("LOCAL_API_ADDR", "", "http")
LocalAPIAllowNonLoopback = env.GetEnvBool("LOCAL_API_ALLOW_NON_LOOPBACK", false)
APIJWTSecure = env.GetEnvBool("API_JWT_SECURE", true)
APIJWTSecret = decodeJWTKey(env.GetEnvString("API_JWT_SECRET", ""))

View File

@@ -30,7 +30,6 @@ type Config struct {
ACL *acl.Config
AutoCert *autocert.Config
Entrypoint entrypoint.Config
InboundMTLSProfiles map[string]types.InboundMTLSProfile
Providers Providers
MatchDomains []string
Homepage homepage.Config
@@ -72,8 +71,6 @@ type State interface {
}
```
`StartAPIServers` starts the authenticated API from `common.APIHTTPAddr` and, when `LOCAL_API_ADDR` is set, an additional **unauthenticated** local listener from `common.LocalAPIHTTPAddr`. That address is validated for loopback binds (with optional DNS resolution); non-loopback requires `LOCAL_API_ALLOW_NON_LOOPBACK` and logs a warning. See [`internal/api/v1/README.md`](../api/v1/README.md#configuration-surface).
### Exported functions
```go

View File

@@ -1,94 +0,0 @@
package config_test
import (
"context"
"iter"
"testing"
"github.com/stretchr/testify/require"
config "github.com/yusing/godoxy/internal/config/types"
entrypointtypes "github.com/yusing/godoxy/internal/entrypoint/types"
routeimpl "github.com/yusing/godoxy/internal/route"
route "github.com/yusing/godoxy/internal/route/types"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/task"
)
func TestRouteValidateInboundMTLSProfile(t *testing.T) {
prev := config.WorkingState.Load()
t.Cleanup(func() {
if prev != nil {
config.WorkingState.Store(prev)
}
})
t.Run("rejects unknown profile", func(t *testing.T) {
state := &stubState{cfg: &config.Config{
InboundMTLSProfiles: map[string]types.InboundMTLSProfile{
"known": {UseSystemCAs: true},
},
}}
config.WorkingState.Store(state)
r := &routeimpl.Route{
Alias: "test",
Scheme: route.SchemeHTTP,
Host: "example.com",
Port: route.Port{Proxy: 80},
InboundMTLSProfile: "missing",
}
err := r.Validate()
require.Error(t, err)
require.ErrorContains(t, err, `inbound mTLS profile "missing" not found`)
})
t.Run("rejects route profile when global profile configured", func(t *testing.T) {
state := &stubState{cfg: &config.Config{
InboundMTLSProfiles: map[string]types.InboundMTLSProfile{
"corp": {UseSystemCAs: true},
},
}}
state.cfg.Entrypoint.InboundMTLSProfile = "corp"
config.WorkingState.Store(state)
r := &routeimpl.Route{
Alias: "test",
Scheme: route.SchemeHTTP,
Host: "example.com",
Port: route.Port{Proxy: 80},
InboundMTLSProfile: "corp",
}
err := r.Validate()
require.Error(t, err)
require.ErrorContains(t, err, "route inbound_mtls_profile is not supported")
})
}
type stubState struct {
cfg *config.Config
}
func (s *stubState) InitFromFile(string) error { return nil }
func (s *stubState) Init([]byte) error { return nil }
func (s *stubState) Task() *task.Task { return nil }
func (s *stubState) Context() context.Context { return context.Background() }
func (s *stubState) Value() *config.Config { return s.cfg }
func (s *stubState) Entrypoint() entrypointtypes.Entrypoint { return nil }
func (s *stubState) ShortLinkMatcher() config.ShortLinkMatcher { return nil }
func (s *stubState) AutoCertProvider() server.CertProvider { return nil }
func (s *stubState) LoadOrStoreProvider(string, types.RouteProvider) (types.RouteProvider, bool) {
return nil, false
}
func (s *stubState) DeleteProvider(string) { /* no-op: test stub */ }
func (s *stubState) IterProviders() iter.Seq2[string, types.RouteProvider] {
// no-op: returns empty iterator
return func(func(string, types.RouteProvider) bool) {}
}
func (s *stubState) NumProviders() int { return 0 } // no-op: test stub
func (s *stubState) StartProviders() error { return nil } // no-op: test stub
func (s *stubState) FlushTmpLog() { /* no-op: test stub */ }
func (s *stubState) StartAPIServers() { /* no-op: test stub */ }
func (s *stubState) StartMetrics() { /* no-op: test stub */ }
var _ config.State = (*stubState)(nil)

View File

@@ -1,77 +0,0 @@
package config
import "testing"
func TestValidateLocalAPIAddr(t *testing.T) {
tests := []struct {
name string
addr string
allowNonLoopback bool
wantErr bool
}{
{
name: "localhost",
addr: "localhost:8888",
},
{
name: "ipv4_loopback",
addr: "127.0.0.1:8888",
},
{
name: "ipv6_loopback",
addr: "[::1]:8888",
},
{
name: "all_interfaces",
addr: ":8888",
wantErr: true,
},
{
name: "all_interfaces_allowed",
addr: ":8888",
allowNonLoopback: true,
},
{
name: "ipv4_unspecified",
addr: "0.0.0.0:8888",
wantErr: true,
},
{
name: "ipv4_unspecified_allowed",
addr: "0.0.0.0:8888",
allowNonLoopback: true,
},
{
name: "lan_ip",
addr: "192.168.1.10:8888",
wantErr: true,
},
{
name: "lan_ip_allowed",
addr: "192.168.1.10:8888",
allowNonLoopback: true,
},
{
name: "hostname_not_loopback",
addr: "godoxy.internal:8888",
wantErr: true,
},
{
name: "hostname_not_loopback_allowed",
addr: "godoxy.internal:8888",
allowNonLoopback: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateLocalAPIAddr(tt.addr, tt.allowNonLoopback)
if tt.wantErr && err == nil {
t.Fatalf("expected error for %q", tt.addr)
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error for %q: %v", tt.addr, err)
}
})
}
}

View File

@@ -9,8 +9,6 @@ import (
"fmt"
"io/fs"
"iter"
"net"
"net/netip"
"os"
"strconv"
"strings"
@@ -218,15 +216,6 @@ func (state *state) StartAPIServers() {
// Local API Handler is used for unauthenticated access.
if common.LocalAPIHTTPAddr != "" {
if err := validateLocalAPIAddr(common.LocalAPIHTTPAddr, common.LocalAPIAllowNonLoopback); err != nil {
log.Err(err).Str("addr", common.LocalAPIHTTPAddr).Msg("refusing to start local API server")
return
}
if common.LocalAPIAllowNonLoopback && !isLoopbackLocalAPIHost(common.LocalAPIHTTPAddr) {
log.Warn().
Str("addr", common.LocalAPIHTTPAddr).
Msg("local API server is allowed to bind to non-loopback addresses")
}
_, err := server.StartServer(state.task.Subtask("local_api_server", false), server.Options{
Name: "local_api",
HTTPAddr: common.LocalAPIHTTPAddr,
@@ -238,51 +227,6 @@ func (state *state) StartAPIServers() {
}
}
func validateLocalAPIAddr(addr string, allowNonLoopback bool) error {
if isLoopbackLocalAPIHost(addr) {
return nil
}
host, _, err := net.SplitHostPort(addr)
if err != nil {
return err
}
if allowNonLoopback {
return nil
}
switch strings.ToLower(host) {
case "localhost":
return nil
case "":
return errors.New("local API address must bind to a loopback host, not all interfaces")
}
ip, err := netip.ParseAddr(host)
if err != nil {
return fmt.Errorf("local API address must use a loopback host: %w", err)
}
if !ip.IsLoopback() {
return fmt.Errorf("local API address must bind to a loopback host, got %q", host)
}
return nil
}
func isLoopbackLocalAPIHost(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return false
}
if strings.EqualFold(host, "localhost") {
return true
}
ip, err := netip.ParseAddr(host)
return err == nil && ip.IsLoopback()
}
func (state *state) StartMetrics() {
systeminfo.Poller.Start(state.task)
uptime.Poller.Start(state.task)
@@ -324,7 +268,6 @@ func (state *state) initEntrypoint() error {
errs := gperr.NewBuilder("entrypoint error")
errs.Add(state.entrypoint.SetMiddlewares(epCfg.Middlewares))
errs.Add(state.entrypoint.SetAccessLogger(state.task, epCfg.AccessLog))
errs.Add(state.entrypoint.SetInboundMTLSProfiles(state.Config.InboundMTLSProfiles))
return errs.Error()
}

View File

@@ -22,7 +22,6 @@ type (
ACL *acl.Config `json:"acl"`
AutoCert *autocert.Config `json:"autocert"`
Entrypoint entrypoint.Config `json:"entrypoint"`
InboundMTLSProfiles map[string]types.InboundMTLSProfile `json:"inbound_mtls_profiles"`
Providers Providers `json:"providers"`
MatchDomains []string `json:"match_domains" validate:"domain_name"`
Homepage homepage.Config `json:"homepage"`

View File

@@ -1,33 +1,31 @@
module github.com/yusing/godoxy/internal/dnsproviders
go 1.26.2
go 1.26.0
replace github.com/yusing/godoxy => ../..
require (
github.com/go-acme/lego/v4 v4.35.1
github.com/yusing/godoxy v0.28.0
github.com/go-acme/lego/v4 v4.32.0
github.com/yusing/godoxy v0.26.0
)
require (
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.1 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang/v13 v13.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/bodgit/tsig v1.2.2 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
@@ -35,13 +33,13 @@ require (
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // 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/stdr v1.2.2 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.2 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/go-resty/resty/v2 v2.17.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
@@ -50,70 +48,59 @@ require (
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
github.com/gotify/server/v2 v2.9.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/gotify/server/v2 v2.9.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/linode/linodego v1.68.0 // indirect
github.com/linode/linodego v1.65.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/maxatome/go-testdeep v1.14.0 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/goinwx v0.12.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.113.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.113.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b // indirect
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/otp v1.5.0 // indirect
github.com/puzpuzpuz/xsync/v4 v4.5.0 // indirect
github.com/rs/zerolog v1.35.1 // indirect
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/vultr/govultr/v3 v3.31.1 // indirect
github.com/vultr/govultr/v3 v3.27.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusing/gointernals v0.2.0 // indirect
github.com/yusing/goutils v0.7.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/arch v0.26.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/api v0.276.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect
google.golang.org/grpc v1.80.0 // indirect
golang.org/x/arch v0.24.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/api v0.267.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

View File

@@ -1,18 +1,18 @@
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
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/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/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
@@ -25,40 +25,35 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.1 h1:edShSHV3DV90+kt+CMaEXEzR9QF7wFrPJxVGz2blMIU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.1/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/akamai/AkamaiOPEN-edgegrid-golang/v13 v13.1.0 h1:KvfpO2utLmpRq0fbC0UZRzdCERfLGLX1/dcYvG7pP7k=
github.com/akamai/AkamaiOPEN-edgegrid-golang/v13 v13.1.0/go.mod h1:AxGyKKxAxaCNeGadscLgo+gBYEAKhNG6tRR5O0HjV30=
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I=
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bodgit/tsig v1.2.2 h1:RgxTCr8UFUHyU4D8Ygb2UtXtS4niw4B6XYYBpgCjl0k=
github.com/bodgit/tsig v1.2.2/go.mod h1:rIGNOLZOV/UA03fmCUtEFbpWOrIoaOuETkpaeTvnLF4=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815/go.mod h1:wYFFK4LYXbX7j+76mOq7aiC/EAw2S22CrzPHqgsisPw=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
@@ -67,12 +62,11 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-acme/lego/v4 v4.35.1 h1:xTcEBsENs4Ek5EiNpN3JXy7W5aynf5KhJ6D/HuuhXv4=
github.com/go-acme/lego/v4 v4.35.1/go.mod h1:E+4l5mPPg9dwhTz6JJptNLvtj94E2JDObI50QuFJd3s=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -85,14 +79,15 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
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-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
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/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@@ -108,48 +103,20 @@ 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gotify/server/v2 v2.9.1 h1:wsQUCdYJ4ZvP7RIRKDLtAtmFQc3kxbrv3QqccO5RWzs=
github.com/gotify/server/v2 v2.9.1/go.mod h1:8scw0hiExomp4rJDrXBwRIcgQm7kv74P4Z4B+iM4l8w=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
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/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/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.3/go.mod h1:dqRwJGXznQrzw6cWmyo6kH+E7jksEQG/CyVWsJEsJO0=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
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/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@@ -164,15 +131,17 @@ 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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/linode/linodego v1.68.0 h1:lAsXuHm/cwQT3KCbVpMGtRiH8IpQl4hUuBOXpqkuNwo=
github.com/linode/linodego v1.68.0/go.mod h1:X7nmTNq1GmZT4bG6w9aiuVrOnhVxYaywrzxM+buC/qU=
github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -181,29 +150,29 @@ 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/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.113.0 h1:OLlJVGHkTHBCXdTGpNn5ay4DV3gOZrVLxlUM6xBQrIM=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.113.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.113.0 h1:bveOZN6gZZjjEM1T9o1TUm8de3zePyDbfuluMpaRJCE=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.113.0/go.mod h1:Ff6Cxm43tuGJHyltQwD3EXLs7kKpkxzqy6DGQcaiS/0=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc=
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/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b h1:it0YPE/evO6/m8t8wxis9KFI2F/aleOKsI6d9uz0cEk=
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b/go.mod h1:tNrEB5k8SI+g5kOlsCmL2ELASfpqEofI0+FLBgBdN08=
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
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/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg=
github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/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/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
@@ -218,123 +187,78 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/vultr/govultr/v3 v3.31.1 h1:AoJRZ0WDS1J1otp2wk3OD4aYh4EQxFU+kvyOPdCe4+Y=
github.com/vultr/govultr/v3 v3.31.1/go.mod h1:2zyUw9yADQaGwKnwDesmIOlBNLrm7edsCfWHFJpWKf8=
github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
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/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtSVvg=
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/go.mod h1:CtF/KFH4q8jkr7cvBpkaExnudE0lLu8sLe43F73Bn5Q=
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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
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/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
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/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/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4=
golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
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/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
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/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.276.0 h1:nVArUtfLEihtW+b0DdcqRGK1xoEm2+ltAihyztq7MKY=
google.golang.org/api v0.276.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
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/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
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/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -1,11 +1,8 @@
package docker
import (
"cmp"
"errors"
"fmt"
"maps"
"slices"
"strconv"
"strings"
@@ -18,19 +15,13 @@ var ErrInvalidLabel = errors.New("invalid label")
const nsProxyDot = NSProxy + "."
type UnexpectedTypeError struct {
Expected string
Actual any
// Message, if non-empty, is returned by Error() instead of the default "expect …, got …" form.
Message string
}
func (e UnexpectedTypeError) Error() string {
if e.Message != "" {
return e.Message
}
return fmt.Sprintf("expect %s, got %T", e.Expected, e.Actual)
var refPrefixes = func() []string {
prefixes := make([]string, 100)
for i := range prefixes {
prefixes[i] = nsProxyDot + "#" + strconv.Itoa(i+1) + "."
}
return prefixes
}()
func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, error) {
nestedMap := make(types.LabelMap)
@@ -38,125 +29,44 @@ func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, e
ExpandWildcard(labels, aliases...)
keys := slices.SortedFunc(maps.Keys(labels), compareLabelKeys)
for _, lbl := range keys {
if err := applyLabel(nestedMap, lbl, labels[lbl]); err != nil {
errs.AddSubject(err, lbl)
for lbl, value := range labels {
parts := strings.Split(lbl, ".")
if parts[0] != NSProxy {
continue
}
if len(parts) == 1 {
errs.AddSubject(ErrInvalidLabel, lbl)
continue
}
parts = parts[1:]
currentMap := nestedMap
for i, k := range parts {
if i == len(parts)-1 {
// Last element, set the value
currentMap[k] = value
} else {
// If the key doesn't exist, create a new map
if _, exists := currentMap[k]; !exists {
currentMap[k] = make(types.LabelMap)
}
// Move deeper into the nested map
m, ok := currentMap[k].(types.LabelMap)
if !ok && currentMap[k] != "" {
errs.AddSubject(fmt.Errorf("expect mapping, got %T", currentMap[k]), lbl)
continue
} else if !ok {
m = make(types.LabelMap)
currentMap[k] = m
}
currentMap = m
}
}
}
return nestedMap, errs.Error()
}
func applyLabel(dst types.LabelMap, lbl, value string) error {
parts := strings.Split(lbl, ".")
if parts[0] != NSProxy {
return nil
}
if len(parts) == 1 {
return ErrInvalidLabel
}
currentMap := dst
for _, part := range parts[1 : len(parts)-1] {
nextMap, err := descendLabelMap(currentMap, part)
if err != nil {
return err
}
currentMap = nextMap
}
return setLabelValue(currentMap, parts[len(parts)-1], value)
}
func descendLabelMap(currentMap types.LabelMap, key string) (types.LabelMap, error) {
if next, ok := currentMap[key]; ok {
switch typed := next.(type) {
case types.LabelMap:
return typed, nil
case string:
objectValue, isObject := parseLabelObject(typed)
if !isObject {
return nil, UnexpectedTypeError{Expected: "mapping", Actual: next}
}
currentMap[key] = objectValue
return objectValue, nil
default:
return nil, UnexpectedTypeError{Expected: "mapping", Actual: next}
}
}
nextMap := make(types.LabelMap)
currentMap[key] = nextMap
return nextMap, nil
}
func setLabelValue(currentMap types.LabelMap, key, value string) error {
existing, ok := currentMap[key].(types.LabelMap)
if !ok {
currentMap[key] = value
return nil
}
objectValue, isObject := parseLabelObject(value)
if !isObject {
return UnexpectedTypeError{Expected: "mapping", Actual: value}
}
return mergeLabelMaps(existing, objectValue)
}
func parseLabelObject(value string) (types.LabelMap, bool) {
if value == "" {
return make(types.LabelMap), true
}
objectValue := make(types.LabelMap)
if err := yaml.Unmarshal([]byte(strings.ReplaceAll(value, "\t", " ")), &objectValue); err != nil {
return nil, false
}
return objectValue, true
}
func mergeLabelMaps(dst, src types.LabelMap) error {
for key, srcValue := range src {
existingValue, exists := dst[key]
if !exists {
dst[key] = srcValue
continue
}
existingMap, existingIsMap := existingValue.(types.LabelMap)
srcMap, srcIsMap := srcValue.(types.LabelMap)
if existingIsMap && srcIsMap {
if err := mergeLabelMaps(existingMap, srcMap); err != nil {
return err
}
continue
}
if existingIsMap {
return UnexpectedTypeError{Expected: "mapping", Actual: srcValue}
}
if srcIsMap {
return UnexpectedTypeError{
Expected: "scalar",
Actual: srcValue,
Message: fmt.Sprintf(
"cannot merge mapping into existing scalar; merge source is %T",
srcValue,
),
}
}
}
return nil
}
func compareLabelKeys(a, b string) int {
if parts := cmp.Compare(strings.Count(a, "."), strings.Count(b, ".")); parts != 0 {
return parts
}
return cmp.Compare(a, b)
}
func ExpandWildcard(labels map[string]string, aliases ...string) {
aliasSet := make(map[string]int, len(aliases))
for i, alias := range aliases {
@@ -167,10 +77,12 @@ func ExpandWildcard(labels map[string]string, aliases ...string) {
// First pass: collect wildcards and discover aliases
for lbl, value := range labels {
alias, suffix, ok := splitAliasLabel(lbl)
if !ok {
if !strings.HasPrefix(lbl, nsProxyDot) {
continue
}
// lbl is "proxy.X..." where X is alias or wildcard
rest := lbl[len(nsProxyDot):] // "X..." or "X.suffix"
alias, suffix, _ := strings.Cut(rest, ".")
if alias == WildcardAlias {
delete(labels, lbl)
if suffix == "" || strings.Count(value, "\n") > 1 {
@@ -196,10 +108,15 @@ func ExpandWildcard(labels map[string]string, aliases ...string) {
// Second pass: convert explicit labels to #N format
for lbl, value := range labels {
alias, suffix, ok := splitAliasLabel(lbl)
if !ok || suffix == "" || alias == "" || alias[0] == '#' {
if !strings.HasPrefix(lbl, nsProxyDot) {
continue
}
rest := lbl[len(nsProxyDot):]
alias, suffix, ok := strings.Cut(rest, ".")
if !ok || alias == "" || alias[0] == '#' {
continue
}
idx, known := aliasSet[alias]
if !known {
continue
@@ -207,27 +124,18 @@ func ExpandWildcard(labels map[string]string, aliases ...string) {
delete(labels, lbl)
if _, overridden := wildcardLabels[suffix]; !overridden {
labels[refPrefix(idx)+suffix] = value
labels[refPrefixes[idx]+suffix] = value
}
}
// Expand wildcards for all aliases
for suffix, value := range wildcardLabels {
for _, idx := range aliasSet {
labels[refPrefix(idx)+suffix] = value
labels[refPrefixes[idx]+suffix] = value
}
}
}
func splitAliasLabel(lbl string) (alias, suffix string, ok bool) {
rest, ok := strings.CutPrefix(lbl, nsProxyDot)
if !ok {
return "", "", false
}
alias, suffix, _ = strings.Cut(rest, ".")
return alias, suffix, true
}
// expandYamlWildcard parses a YAML document in value, flattens it to dot-notated keys and adds the
// results into dest map where each key is the flattened suffix and the value is the scalar string
// representation. The provided YAML is expected to be a mapping.
@@ -244,53 +152,59 @@ func expandYamlWildcard(value string, dest map[string]string) {
flattenMap("", raw, dest)
}
// refPrefix returns the prefix for a reference to the Nth alias.
func refPrefix(n int) string {
return nsProxyDot + "#" + strconv.Itoa(n+1) + "."
}
// flattenMap converts nested maps into a flat map with dot-delimited keys.
func flattenMap(prefix string, src map[string]any, dest map[string]string) {
for k, v := range src {
flattenValue(joinLabelKey(prefix, k), v, dest)
key := k
if prefix != "" {
key = prefix + "." + k
}
switch vv := v.(type) {
case map[string]any:
flattenMap(key, vv, dest)
case map[any]any:
flattenMapAny(key, vv, dest)
case string:
dest[key] = vv
case int:
dest[key] = strconv.Itoa(vv)
case bool:
dest[key] = strconv.FormatBool(vv)
case float64:
dest[key] = strconv.FormatFloat(vv, 'f', -1, 64)
default:
dest[key] = fmt.Sprint(v)
}
}
}
func flattenMapAny(prefix string, src map[any]any, dest map[string]string) {
for k, v := range src {
flattenValue(joinLabelKey(prefix, stringifyLabelKey(k)), v, dest)
}
}
func flattenValue(key string, value any, dest map[string]string) {
switch typed := value.(type) {
case map[string]any:
flattenMap(key, typed, dest)
case map[any]any:
flattenMapAny(key, typed, dest)
var key string
switch kk := k.(type) {
case string:
dest[key] = typed
case int:
dest[key] = strconv.Itoa(typed)
case bool:
dest[key] = strconv.FormatBool(typed)
case float64:
dest[key] = strconv.FormatFloat(typed, 'f', -1, 64)
key = kk
default:
dest[key] = fmt.Sprint(value)
key = fmt.Sprint(k)
}
if prefix != "" {
key = prefix + "." + key
}
switch vv := v.(type) {
case map[string]any:
flattenMap(key, vv, dest)
case map[any]any:
flattenMapAny(key, vv, dest)
case string:
dest[key] = vv
case int:
dest[key] = strconv.Itoa(vv)
case bool:
dest[key] = strconv.FormatBool(vv)
case float64:
dest[key] = strconv.FormatFloat(vv, 'f', -1, 64)
default:
dest[key] = fmt.Sprint(v)
}
}
func joinLabelKey(prefix, key string) string {
if prefix == "" {
return key
}
return prefix + "." + key
}
func stringifyLabelKey(key any) string {
if typed, ok := key.(string); ok {
return typed
}
return fmt.Sprint(key)
}

View File

@@ -1,310 +0,0 @@
package docker
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/yusing/godoxy/internal/types"
)
func TestParseLabelsIgnoresNonProxyAndRejectsInvalidRoot(t *testing.T) {
parsed, err := ParseLabels(map[string]string{
"other.label": "value",
"proxy": "invalid",
})
require.ErrorIs(t, err, ErrInvalidLabel)
require.Empty(t, parsed)
}
func TestParseLabelsPromotesEmptyStringIntoNestedObject(t *testing.T) {
parsed, err := ParseLabels(map[string]string{
"proxy.a.b": "",
"proxy.a.b.c": "value",
})
require.NoError(t, err)
require.Equal(t, types.LabelMap{
"a": types.LabelMap{
"b": types.LabelMap{
"c": "value",
},
},
}, parsed)
}
func TestParseLabelsMergesObjectIntoExistingMap(t *testing.T) {
parsed, err := ParseLabels(map[string]string{
"proxy.a.b": "c: generic\nd: merged",
"proxy.a.b.c": "specific",
})
require.NoError(t, err)
require.Equal(t, types.LabelMap{
"a": types.LabelMap{
"b": types.LabelMap{
"c": "specific",
"d": "merged",
},
},
}, parsed)
}
func TestParseLabelsRejectsInvalidObjectMergeValue(t *testing.T) {
parsed, err := ParseLabels(map[string]string{
"proxy.a.b": "- invalid",
"proxy.a.b.c": "specific",
})
require.ErrorContains(t, err, "proxy.a.b.c")
require.ErrorContains(t, err, "expect mapping, got string")
require.Equal(t, types.LabelMap{
"a": types.LabelMap{
"b": "- invalid",
},
}, parsed)
}
func TestParseLabelsRejectsSpecificFieldOverrideOfNestedObjectField(t *testing.T) {
parsed, err := ParseLabels(map[string]string{
"proxy.a.b": "c:\n nested: value",
"proxy.a.b.c": "specific",
})
require.ErrorContains(t, err, "proxy.a.b.c")
require.ErrorContains(t, err, "expect mapping, got string")
require.Equal(t, types.LabelMap{
"a": types.LabelMap{
"b": types.LabelMap{
"c": types.LabelMap{
"nested": "value",
},
},
},
}, parsed)
}
func TestParseLabelsMergesIntoExistingNestedMap(t *testing.T) {
parsed, err := ParseLabels(map[string]string{
"proxy.a.b": "c:\n nested:\n allow: true",
"proxy.a.b.c": "nested:\n deny: true",
})
require.NoError(t, err)
require.Equal(t, types.LabelMap{
"a": types.LabelMap{
"b": types.LabelMap{
"c": types.LabelMap{
"nested": types.LabelMap{
"allow": true,
"deny": true,
},
},
},
},
}, parsed)
}
func TestParseLabelsRejectsInvalidNestedObjectMergeValue(t *testing.T) {
parsed, err := ParseLabels(map[string]string{
"proxy.a.b": "c:\n nested: value",
"proxy.a.b.c": "- invalid",
})
require.ErrorContains(t, err, "proxy.a.b.c")
require.ErrorContains(t, err, "expect mapping, got string")
require.Equal(t, types.LabelMap{
"a": types.LabelMap{
"b": types.LabelMap{
"c": types.LabelMap{
"nested": "value",
},
},
},
}, parsed)
}
func TestParseLabelsRejectsConflictingNestedObjectMerge(t *testing.T) {
parsed, err := ParseLabels(map[string]string{
"proxy.a.b": "c:\n nested:\n allow: true",
"proxy.a.b.c": "nested: blocked",
})
require.ErrorContains(t, err, "proxy.a.b.c")
require.ErrorContains(t, err, "expect mapping, got string")
require.Equal(t, types.LabelMap{
"a": types.LabelMap{
"b": types.LabelMap{
"c": types.LabelMap{
"nested": types.LabelMap{
"allow": true,
},
},
},
},
}, parsed)
}
func TestParseLabelsRejectsNestedFieldInsideScalarObjectMember(t *testing.T) {
parsed, err := ParseLabels(map[string]string{
"proxy.a.b": "c: 1",
"proxy.a.b.c.d": "value",
})
require.ErrorContains(t, err, "proxy.a.b.c.d")
require.ErrorContains(t, err, "expect mapping, got uint64")
require.Equal(t, types.LabelMap{
"a": types.LabelMap{
"b": types.LabelMap{
"c": uint64(1),
},
},
}, parsed)
}
func TestParseLabelObject(t *testing.T) {
t.Run("empty string becomes empty map", func(t *testing.T) {
parsed, ok := parseLabelObject("")
require.True(t, ok)
require.Empty(t, parsed)
})
t.Run("yaml object parses", func(t *testing.T) {
parsed, ok := parseLabelObject("nested:\n\tvalue: true")
require.True(t, ok)
require.Equal(t, types.LabelMap{
"nested": types.LabelMap{
"value": true,
},
}, parsed)
})
t.Run("non-object yaml is rejected", func(t *testing.T) {
parsed, ok := parseLabelObject("- item")
require.False(t, ok)
require.Nil(t, parsed)
})
}
func TestMergeLabelMaps(t *testing.T) {
t.Run("recursively merges nested maps and preserves specific scalar overrides", func(t *testing.T) {
dst := types.LabelMap{
"allowed_groups": []any{"specific"},
"bypass": types.LabelMap{
"path": "/private",
},
}
src := types.LabelMap{
"allowed_groups": []any{"generic"},
"bypass": types.LabelMap{
"methods": "GET",
},
"priority": 5,
}
err := mergeLabelMaps(dst, src)
require.NoError(t, err)
require.Equal(t, types.LabelMap{
"allowed_groups": []any{"specific"},
"bypass": types.LabelMap{
"path": "/private",
"methods": "GET",
},
"priority": 5,
}, dst)
})
t.Run("rejects map receiving scalar", func(t *testing.T) {
err := mergeLabelMaps(types.LabelMap{
"bypass": types.LabelMap{"path": "/private"},
}, types.LabelMap{
"bypass": "skip",
})
require.ErrorContains(t, err, "expect mapping")
})
t.Run("rejects scalar receiving map", func(t *testing.T) {
err := mergeLabelMaps(types.LabelMap{
"bypass": "skip",
}, types.LabelMap{
"bypass": types.LabelMap{"path": "/private"},
})
require.ErrorContains(t, err, "cannot merge mapping into existing scalar")
})
t.Run("rejects nested recursive map conflicts", func(t *testing.T) {
err := mergeLabelMaps(types.LabelMap{
"outer": types.LabelMap{
"nested": types.LabelMap{"allow": true},
},
}, types.LabelMap{
"outer": types.LabelMap{
"nested": "blocked",
},
})
require.ErrorContains(t, err, "expect mapping")
})
}
func TestCompareLabelKeys(t *testing.T) {
require.Less(t, compareLabelKeys("proxy.a", "proxy.a.b"), 0)
require.Less(t, compareLabelKeys("proxy.a.a", "proxy.a.b"), 0)
require.Greater(t, compareLabelKeys("proxy.a.c", "proxy.a.b"), 0)
}
func TestFlattenMapAny(t *testing.T) {
dest := make(map[string]string)
flattenMapAny("", map[any]any{
"nested": map[any]any{
"string": "value",
"int": 7,
"bool": true,
"float": 1.5,
9: "numeric-key",
"map": map[string]any{
"child": "value",
},
},
"list": []int{1, 2},
}, dest)
require.Equal(t, map[string]string{
"nested.string": "value",
"nested.int": "7",
"nested.bool": "true",
"nested.float": "1.5",
"nested.9": "numeric-key",
"nested.map.child": "value",
"list": "[1 2]",
}, dest)
}
func TestFlattenMap(t *testing.T) {
dest := make(map[string]string)
flattenMap("", map[string]any{
"nested": map[string]any{
"string": "value",
"mapany": map[any]any{
"child": "nested-value",
},
"int": 7,
"bool": true,
"float": 1.5,
},
"list": []int{1, 2},
}, dest)
require.Equal(t, map[string]string{
"nested.string": "value",
"nested.mapany.child": "nested-value",
"nested.int": "7",
"nested.bool": "true",
"nested.float": "1.5",
"list": "[1 2]",
}, dest)
}

View File

@@ -1,7 +1,6 @@
package docker_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
@@ -243,14 +242,6 @@ port: 8080`[1:]
})
}
func requireMap(t *testing.T, value any) map[string]any {
t.Helper()
m, ok := value.(map[string]any)
require.True(t, ok, "expected map[string]any, got %T", value)
return m
}
func BenchmarkParseLabels(b *testing.B) {
m := map[string]string{
"proxy.a.host": "localhost",
@@ -262,39 +253,3 @@ func BenchmarkParseLabels(b *testing.B) {
_, _ = docker.ParseLabels(m, "a", "b")
}
}
func TestParseLabelsMixedObjectAndFlatFields(t *testing.T) {
for i := range 100 {
labels := map[string]string{
"proxy.universal.middlewares.oidc": "allowed_groups: [everyone]",
"proxy.universal.middlewares.oidc.bypass": "- path glob(/geheimenvan/*)",
}
parsed, err := docker.ParseLabels(labels)
require.NoError(t, err, fmt.Sprintf("iteration %d", i))
universal := requireMap(t, parsed["universal"])
middlewares := requireMap(t, universal["middlewares"])
oidc := requireMap(t, middlewares["oidc"])
require.Equal(t, []any{"everyone"}, oidc["allowed_groups"])
require.Equal(t, "- path glob(/geheimenvan/*)", oidc["bypass"])
}
}
func TestParseLabelsRejectsScalarAndNestedObjectConflict(t *testing.T) {
for i := range 100 {
parsed, err := docker.ParseLabels(map[string]string{
"proxy.universal.middlewares.oidc": "bypass: skip",
"proxy.universal.middlewares.oidc.bypass.path": "/geheimenvan",
})
require.ErrorContains(t, err, "proxy.universal.middlewares.oidc.bypass.path")
require.ErrorContains(t, err, "expect mapping, got string")
universal := requireMap(t, parsed["universal"])
middlewares := requireMap(t, universal["middlewares"])
oidc := requireMap(t, middlewares["oidc"])
require.Equal(t, "skip", oidc["bypass"], "iteration %d", i)
}
}

View File

@@ -11,7 +11,6 @@ The entrypoint package implements the primary HTTP handler that receives all inc
- Domain-based route lookup with subdomain support
- Short link (`go/<alias>` domain) handling
- Middleware chain application
- Route-specific promotion of middleware overlays into matching entrypoint middleware
- Access logging for all requests
- Configurable not-found handling
- Per-domain route resolution
@@ -94,7 +93,6 @@ type RWPoolLike[Route types.Route] interface {
```go
type Config struct {
SupportProxyProtocol bool `json:"support_proxy_protocol"`
InboundMTLSProfile string `json:"inbound_mtls_profile,omitempty"`
Rules struct {
NotFound rules.Rules `json:"not_found"`
} `json:"rules"`
@@ -103,90 +101,6 @@ type Config struct {
}
```
### Entrypoint middleware bypass overlays
For HTTP routes, the entrypoint can compile a route-specific effective middleware chain when a route contributes a local middleware entry whose name matches an existing entrypoint middleware and whose options include `bypass`.
Behavior is intentionally narrow:
- only `bypass` is promoted in v1
- promotion is **append-only**
- the entrypoint middleware must already exist
- if no matching entrypoint middleware exists, route-local behavior stays unchanged
- when the route-local middleware entry is bypass-only, it is consumed after promotion so the same middleware is not evaluated twice
Example:
```yaml
entrypoint:
middlewares:
- use: oidc
routes:
app:
middlewares:
oidc:
bypass:
- path glob("/public/*")
```
This behaves as if the entrypoint middleware for that route had:
```yaml
entrypoint:
middlewares:
- use: oidc
bypass:
- route app & path glob("/public/*")
```
Pre-existing entrypoint bypass rules remain active; route bypass rules are added on top.
`InboundMTLSProfile` references a named root-level inbound mTLS profile and enables Go's built-in client-certificate verification (`tls.RequireAndVerifyClientCert`) for all HTTPS traffic on the entrypoint.
- When configured, route-level inbound mTLS overrides are not supported.
- Without a global profile, route-level inbound mTLS may still select profiles by TLS SNI.
- For a route that enforces client certificates, the route matched from the HTTP `Host` and the route matched from TLS SNI must be the same route (compared by route identity/key after `FindRoute`). That resolution is the entrypoint's route table, not DNS or any external name resolution.
### Inbound mTLS profiles
Root config provides reusable named inbound mTLS profiles via `config.Config.InboundMTLSProfiles`. Each profile is a [`types.InboundMTLSProfile`](internal/types/inbound_mtls.go): optional system trust roots plus zero or more PEM CA certificate files on disk (`ca_files`). `SetInboundMTLSProfiles` compiles those profiles into certificate pools, and the TLS server sets `ClientCAs` from the selected pool.
PEM content is not embedded in YAML: list file paths under `ca_files`; each file should contain one or more PEM-encoded CA certificates.
```yaml
inbound_mtls_profiles:
corp-clients:
use_system_cas: false
ca_files:
- /etc/godoxy/mtls/corp-root-ca.pem
- /etc/godoxy/mtls/corp-issuing-ca.pem
corp-plus-extra:
use_system_cas: true
ca_files:
- /etc/godoxy/mtls/private-intermediate.pem
```
Apply one profile to **all** HTTPS listeners by naming it on the entrypoint:
```yaml
entrypoint:
inbound_mtls_profile: corp-clients
```
#### Security considerations
- **Client certificates and chain verification** — The server requires a client certificate and verifies it with Go's TLS stack. The chain must build to one of the CAs in the selected pool (custom PEMs from `ca_files`, and optionally the OS trust store when `use_system_cas` is true). Leaf validity (time, EKU, and related checks) follows standard Go behavior for client-auth verification.
- **CA management and rotation** — CA material is read from the filesystem when profiles are compiled during config load / entrypoint setup. Updating trust for a running process requires a config reload or restart so the new PEM files are read.
- **CRL / OCSP revocation** — Go's standard inbound mTLS verification does not perform CRL or OCSP checks for client certificates, and GoDoxy does not add a custom revocation layer.
- **Misconfigured trust pools** — A pool that is too broad (for example `use_system_cas: true` with few constraints) can trust far more clients than intended. A pool that omits required intermediates can reject otherwise valid clients.
#### Failure modes
- **Invalid or unreadable CA material** — Missing files, non-PEM content, or PEM that does not parse as CA certificates cause profile compilation to fail. `SetInboundMTLSProfiles` returns collected per-profile errors.
- **Missing profile referenced by entrypoint** — If `entrypoint.inbound_mtls_profile` names a profile that is not present in `inbound_mtls_profiles`, initialization returns `entrypoint inbound mTLS profile "<name>" not found`.
- **Client certificate validation failures** — Clients that omit a cert, present a cert that does not chain to the configured pool, or fail other TLS checks see a failed TLS handshake before HTTP handling starts.
### Context Functions
```go
@@ -208,9 +122,9 @@ classDiagram
+accessLogger AccessLogger
+findRouteFunc findRouteFunc
+shortLinkMatcher *ShortLinkMatcher
+streamRoutes *pool.Pool\[types.StreamRoute\]
+excludedRoutes *pool.Pool\[types.Route\]
+servers *xsync.Map\[string, *httpServer\]
+streamRoutes *pool.Pool[types.StreamRoute]
+excludedRoutes *pool.Pool[types.Route]
+servers *xsync.Map[string, *httpServer]
+SupportProxyProtocol() bool
+StartAddRoute(r) error
+IterRoutes(yield)
@@ -218,7 +132,7 @@ classDiagram
}
class httpServer {
+routes *pool.Pool\[types.HTTPRoute\]
+routes *pool.Pool[types.HTTPRoute]
+ServeHTTP(w, r)
+AddRoute(route)
+DelRoute(route)
@@ -240,8 +154,8 @@ classDiagram
}
class ShortLinkMatcher {
+fqdnRoutes *xsync.Map\[string, string\]
+subdomainRoutes *xsync.Map\[string, emptyStruct\]
+fqdnRoutes *xsync.Map[string, string]
+subdomainRoutes *xsync.Map[string, struct{}]
+ServeHTTP(w, r)
+AddRoute(alias)
+DelRoute(alias)

View File

@@ -9,7 +9,6 @@ import (
// including proxy protocol support, routing rules, middlewares, and access logging.
type Config struct {
SupportProxyProtocol bool `json:"support_proxy_protocol"`
InboundMTLSProfile string `json:"inbound_mtls_profile,omitempty"`
Rules struct {
NotFound rules.Rules `json:"not_found"`
} `json:"rules"`

View File

@@ -1,8 +1,6 @@
package entrypoint
import (
"crypto/x509"
"maps"
"net/http"
"strings"
"sync/atomic"
@@ -43,8 +41,6 @@ type Entrypoint struct {
httpPoolDisableLog atomic.Bool
servers *xsync.Map[string, *httpServer] // listen addr -> server
inboundMTLSProfiles map[string]*x509.CertPool
}
var _ entrypoint.Entrypoint = &Entrypoint{}
@@ -73,7 +69,6 @@ func NewEntrypoint(parent task.Parent, cfg *Config) *Entrypoint {
streamRoutes: pool.New[types.StreamRoute]("stream_routes", "stream_routes"),
excludedRoutes: pool.New[types.Route]("excluded_routes", "excluded_routes"),
servers: xsync.NewMap[string, *httpServer](),
inboundMTLSProfiles: make(map[string]*x509.CertPool),
}
return ep
}
@@ -133,27 +128,14 @@ func (ep *Entrypoint) SetFindRouteDomains(domains []string) {
func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error {
if len(mws) == 0 {
ep.middleware = nil
ep.cfg.Middlewares = nil
for _, srv := range ep.servers.Range {
srv.resetRouteEntrypointOverlays()
}
return nil
}
tmpMiddlewares := make([]map[string]any, len(mws))
for i, mw := range mws {
tmpMiddlewares[i] = maps.Clone(mw)
}
mid, err := middleware.BuildMiddlewareFromChainRaw("entrypoint", mws)
if err != nil {
return err
}
ep.middleware = mid
ep.cfg.Middlewares = tmpMiddlewares
for _, srv := range ep.servers.Range {
srv.resetRouteEntrypointOverlays()
}
log.Debug().Msg("entrypoint middleware loaded")
return nil

View File

@@ -1,70 +0,0 @@
package entrypoint
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
"github.com/yusing/godoxy/internal/types"
)
func TestSetMiddlewaresInvalidatesRouteOverlayCache(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
srv := newTestHTTPServer(t, ep)
route := newFakeHTTPRoute(t, "test-route", "")
route.routeMiddlewares = map[string]types.LabelMap{
"redirectHTTP": {
"bypass": "- path /health\n",
},
}
route.handler = func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
srv.AddRoute(route)
require.NoError(t, ep.SetMiddlewares([]map[string]any{{
"use": "redirectHTTP",
}}))
first := httptest.NewRecorder()
srv.ServeHTTP(first, httptest.NewRequest(http.MethodGet, "http://test-route/private", nil))
require.Equal(t, http.StatusPermanentRedirect, first.Code)
require.NoError(t, ep.SetMiddlewares([]map[string]any{{
"use": "response",
"set_headers": map[string]string{
"X-Overlay-Reloaded": "true",
},
}}))
second := httptest.NewRecorder()
srv.ServeHTTP(second, httptest.NewRequest(http.MethodGet, "http://test-route/private", nil))
require.Equal(t, http.StatusNoContent, second.Code)
require.Equal(t, "true", second.Header().Get("X-Overlay-Reloaded"))
}
func TestServeHTTPHidesEntrypointOverlayCompilationErrors(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
srv := newTestHTTPServer(t, ep)
route := newFakeHTTPRoute(t, "test-route", "")
route.routeMiddlewares = map[string]types.LabelMap{
"redirectHTTP": {
"bypass": "not-a-valid-bypass",
},
}
route.handler = func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
srv.AddRoute(route)
require.NoError(t, ep.SetMiddlewares([]map[string]any{{
"use": "redirectHTTP",
}}))
rec := httptest.NewRecorder()
srv.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "http://test-route/", nil))
require.Equal(t, http.StatusInternalServerError, rec.Code)
require.Equal(t, "internal server error\n", rec.Body.String())
}

View File

@@ -1,8 +0,0 @@
package entrypoint
import "errors"
var (
errSecureRouteRequiresSNI = errors.New("secure route requires matching TLS SNI")
errSecureRouteMisdirected = errors.New("secure route host must match TLS SNI")
)

View File

@@ -3,12 +3,9 @@ package entrypoint
import (
"errors"
"fmt"
"net"
"net/http"
"strings"
"sync/atomic"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
acl "github.com/yusing/godoxy/internal/acl/types"
autocert "github.com/yusing/godoxy/internal/autocert/types"
@@ -38,20 +35,6 @@ type httpServer struct {
addr string
routes *pool.Pool[types.HTTPRoute]
routeEntrypointOverlays atomic.Pointer[xsync.Map[string, *routeEntrypointOverlay]]
}
type routeEntrypointOverlay struct {
middleware *middleware.Middleware
consumedBypass map[string]struct{}
consumedMiddlewares map[string]struct{}
}
var errNoRouteEntrypointOverlay = errors.New("no route entrypoint overlay")
func newRouteEntrypointOverlayMap() *xsync.Map[string, *routeEntrypointOverlay] {
return xsync.NewMap[string, *routeEntrypointOverlay]()
}
type HTTPProto string
@@ -66,17 +49,11 @@ func NewHTTPServer(ep *Entrypoint) HTTPServer {
}
func newHTTPServer(ep *Entrypoint) *httpServer {
srv := &httpServer{ep: ep}
srv.resetRouteEntrypointOverlays()
return srv
return &httpServer{ep: ep}
}
// Listen starts the server and stop when entrypoint is stopped.
func (srv *httpServer) Listen(addr string, proto HTTPProto) error {
return srv.listen(addr, proto, nil)
}
func (srv *httpServer) listen(addr string, proto HTTPProto, listener net.Listener) error {
if srv.addr != "" {
return errors.New("server already started")
}
@@ -91,12 +68,9 @@ func (srv *httpServer) listen(addr string, proto HTTPProto, listener net.Listene
switch proto {
case HTTPProtoHTTP:
opts.HTTPAddr = addr
opts.HTTPListener = listener
case HTTPProtoHTTPS:
opts.HTTPSAddr = addr
opts.HTTPSListener = listener
opts.CertProvider = autocert.FromCtx(srv.ep.task.Context())
opts.TLSConfigMutator = srv.mutateServerTLSConfig
}
task := srv.ep.task.Subtask("http_server", false)
@@ -120,12 +94,10 @@ func (srv *httpServer) Close() {
func (srv *httpServer) AddRoute(route types.HTTPRoute) {
srv.routes.Add(route)
srv.routeEntrypointOverlayMap().Delete(route.Key())
}
func (srv *httpServer) DelRoute(route types.HTTPRoute) {
srv.routes.Del(route)
srv.routeEntrypointOverlayMap().Delete(route.Key())
}
func (srv *httpServer) FindRoute(s string) types.HTTPRoute {
@@ -144,43 +116,14 @@ func (srv *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}()
}
route, err := srv.resolveRequestRoute(r)
route := srv.ep.findRouteFunc(srv.routes, r.Host)
switch {
case errors.Is(err, errSecureRouteRequiresSNI), errors.Is(err, errSecureRouteMisdirected):
http.Error(w, err.Error(), http.StatusMisdirectedRequest)
return
case err != nil:
log.Err(err).Msg("failed to resolve HTTP route")
http.Error(w, "internal server error", http.StatusInternalServerError)
return
case route != nil:
r = routes.WithRouteContext(r, route)
entrypointMiddleware := srv.ep.middleware
next := route.ServeHTTP
if entrypointMiddleware != nil {
overlay, err := srv.getRouteEntrypointOverlay(route)
if err != nil && !errors.Is(err, errNoRouteEntrypointOverlay) {
log.Err(err).Str("route", route.Name()).Msg("failed to compile route-specific entrypoint middleware")
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
if overlay != nil {
entrypointMiddleware = overlay.middleware
if len(overlay.consumedBypass) > 0 || len(overlay.consumedMiddlewares) > 0 {
next = func(w http.ResponseWriter, req *http.Request) {
route.ServeHTTP(w, middleware.WithConsumedRouteOverlays(
req,
overlay.consumedBypass,
overlay.consumedMiddlewares,
))
}
}
}
}
if entrypointMiddleware != nil {
entrypointMiddleware.ServeHTTP(next, w, r)
if srv.ep.middleware != nil {
srv.ep.middleware.ServeHTTP(route.ServeHTTP, w, r)
} else {
next(w, r)
route.ServeHTTP(w, r)
}
case srv.tryHandleShortLink(w, r):
return
@@ -191,120 +134,6 @@ func (srv *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
func (srv *httpServer) getRouteEntrypointOverlay(route types.HTTPRoute) (*routeEntrypointOverlay, error) {
if srv.ep.middleware == nil || len(srv.ep.cfg.Middlewares) == 0 {
return nil, errNoRouteEntrypointOverlay
}
overlays := srv.routeEntrypointOverlayMap()
var buildErr error
overlay, _ := overlays.LoadOrCompute(route.Key(), func() (*routeEntrypointOverlay, bool) {
computed, err := srv.compileRouteEntrypointOverlay(route)
if err != nil {
buildErr = err
return nil, true
}
return computed, false
})
if buildErr != nil {
return nil, buildErr
}
if overlay.middleware == nil {
return nil, errNoRouteEntrypointOverlay
}
return overlay, nil
}
func (srv *httpServer) routeEntrypointOverlayMap() *xsync.Map[string, *routeEntrypointOverlay] {
overlays := srv.routeEntrypointOverlays.Load()
if overlays != nil {
return overlays
}
overlays = newRouteEntrypointOverlayMap()
if srv.routeEntrypointOverlays.CompareAndSwap(nil, overlays) {
return overlays
}
return srv.routeEntrypointOverlays.Load()
}
func (srv *httpServer) resetRouteEntrypointOverlays() {
srv.routeEntrypointOverlays.Store(newRouteEntrypointOverlayMap())
}
func (srv *httpServer) compileRouteEntrypointOverlay(route types.HTTPRoute) (*routeEntrypointOverlay, error) {
routeMiddlewareMap := route.RouteMiddlewares()
if len(routeMiddlewareMap) == 0 {
return &routeEntrypointOverlay{}, nil
}
compiled, err := middleware.BuildEntrypointRouteOverlay(
"entrypoint",
srv.ep.cfg.Middlewares,
route.Name(),
routeMiddlewareMap,
)
if err != nil {
if errors.Is(err, middleware.ErrNoEntrypointRouteOverlay) {
return &routeEntrypointOverlay{}, nil
}
return nil, err
}
return &routeEntrypointOverlay{
middleware: compiled.Middleware,
consumedBypass: compiled.ConsumedBypass,
consumedMiddlewares: compiled.ConsumedMiddlewares,
}, nil
}
func (srv *httpServer) resolveRequestRoute(req *http.Request) (types.HTTPRoute, error) {
hostRoute := srv.FindRoute(req.Host)
// Skip per-route mTLS resolution if no TLS or a global mTLS profile is configured
if req.TLS == nil || srv.ep.cfg.InboundMTLSProfile != "" {
return hostRoute, nil
}
_, hostSecure, err := srv.resolveInboundMTLSProfileForRoute(hostRoute)
if err != nil {
return nil, err
}
serverName := req.TLS.ServerName
if serverName == "" {
if hostSecure {
return nil, errSecureRouteRequiresSNI
}
return hostRoute, nil
}
sniRoute := srv.FindRoute(serverName)
_, sniSecure, err := srv.resolveInboundMTLSProfileForRoute(sniRoute)
if err != nil {
return nil, err
}
if sniSecure {
if !sameHTTPRoute(hostRoute, sniRoute) {
return nil, errSecureRouteMisdirected
}
return sniRoute, nil
}
if hostSecure {
return nil, errSecureRouteMisdirected
}
return hostRoute, nil
}
func sameHTTPRoute(left, right types.HTTPRoute) bool {
switch {
case left == nil || right == nil:
return left == right
case left == right:
return true
default:
return left.Key() == right.Key()
}
}
func (srv *httpServer) tryHandleShortLink(w http.ResponseWriter, r *http.Request) (handled bool) {
host := r.Host
if before, _, ok := strings.Cut(host, ":"); ok {

View File

@@ -1,184 +0,0 @@
package entrypoint
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
)
func compileInboundMTLSProfiles(profiles map[string]types.InboundMTLSProfile) (map[string]*x509.CertPool, error) {
if len(profiles) == 0 {
return map[string]*x509.CertPool{}, nil
}
compiled := make(map[string]*x509.CertPool, len(profiles))
errs := gperr.NewBuilder("inbound mTLS profiles error")
for name, profile := range profiles {
if err := profile.Validate(); err != nil {
errs.AddSubjectf(err, "profiles.%s", name)
continue
}
pool, err := buildInboundMTLSCAPool(profile)
if err != nil {
errs.AddSubjectf(err, "profiles.%s", name)
continue
}
compiled[name] = pool
}
if err := errs.Error(); err != nil {
return nil, err
}
return compiled, nil
}
func buildInboundMTLSCAPool(profile types.InboundMTLSProfile) (*x509.CertPool, error) {
var pool *x509.CertPool
if profile.UseSystemCAs {
systemPool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
pool = systemPool
}
if pool == nil {
pool = x509.NewCertPool()
}
for _, file := range profile.CAFiles {
data, err := os.ReadFile(file)
if err != nil {
return nil, gperr.PrependSubject(err, file)
}
if !pool.AppendCertsFromPEM(data) {
return nil, gperr.PrependSubject(errors.New("failed to parse CA certificates"), file)
}
}
return pool, nil
}
func (ep *Entrypoint) SetInboundMTLSProfiles(profiles map[string]types.InboundMTLSProfile) error {
compiled, err := compileInboundMTLSProfiles(profiles)
if err != nil {
return err
}
if profileRef := ep.cfg.InboundMTLSProfile; profileRef != "" {
if _, ok := compiled[profileRef]; !ok {
return fmt.Errorf("entrypoint inbound mTLS profile %q not found", profileRef)
}
}
ep.inboundMTLSProfiles = compiled
return nil
}
func (srv *httpServer) mutateServerTLSConfig(base *tls.Config) *tls.Config {
if base == nil {
return base
}
pool, enabled, err := srv.resolveInboundMTLSProfileForGlobal()
switch {
case err != nil:
log.Err(err).Msg("inbound mTLS: failed to resolve global profile, falling back to per-route mTLS")
case enabled:
return applyInboundMTLSProfile(base, pool)
}
cfg := base.Clone()
cfg.GetConfigForClient = func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
pool, enabled, err := srv.resolveInboundMTLSProfileForServerName(hello.ServerName, false)
if err != nil {
return nil, err
}
if enabled {
return applyInboundMTLSProfile(base, pool), nil
}
return cloneTLSConfig(base), nil
}
return cfg
}
func applyInboundMTLSProfile(base *tls.Config, pool *x509.CertPool) *tls.Config {
cfg := cloneTLSConfig(base)
cfg.ClientAuth = tls.RequireAndVerifyClientCert
cfg.ClientCAs = pool
return cfg
}
func cloneTLSConfig(base *tls.Config) *tls.Config {
cfg := base.Clone()
cfg.GetConfigForClient = nil
return cfg
}
func ValidateInboundMTLSProfileRef(profileRef, globalProfile string, profiles map[string]types.InboundMTLSProfile) error {
if profileRef == "" {
return nil
}
if globalProfile != "" {
return errors.New("route inbound_mtls_profile is not supported when entrypoint.inbound_mtls_profile is configured")
}
if _, ok := profiles[profileRef]; !ok {
return fmt.Errorf("inbound mTLS profile %q not found", profileRef)
}
return nil
}
func (srv *httpServer) resolveInboundMTLSProfileForServerName(serverName string, allowGlobal bool) (pool *x509.CertPool, enabled bool, err error) {
if serverName == "" {
if allowGlobal {
return srv.resolveInboundMTLSProfileForGlobal()
}
return nil, false, nil
}
pool, enabled, err = srv.resolveInboundMTLSProfileForRoute(srv.FindRoute(serverName))
if err != nil {
return nil, false, err
}
if enabled || !allowGlobal {
return pool, enabled, nil
}
return srv.resolveInboundMTLSProfileForGlobal()
}
func (srv *httpServer) resolveInboundMTLSProfileForRoute(route types.HTTPRoute) (pool *x509.CertPool, enabled bool, err error) {
if route == nil {
return nil, false, nil
}
if ref := route.InboundMTLSProfileRef(); ref != "" {
if p, ok := srv.lookupInboundMTLSProfile(ref); ok {
return p, true, nil
}
return nil, false, fmt.Errorf("route %q inbound mTLS profile %q not found", route.Name(), ref)
}
return nil, false, nil
}
func (srv *httpServer) resolveInboundMTLSProfileForGlobal() (pool *x509.CertPool, enabled bool, err error) {
if globalRef := srv.ep.cfg.InboundMTLSProfile; globalRef != "" {
if p, ok := srv.lookupInboundMTLSProfile(globalRef); ok {
return p, true, nil
}
return nil, false, fmt.Errorf("entrypoint inbound mTLS profile %q not found", globalRef)
}
return nil, false, nil
}
func (srv *httpServer) lookupInboundMTLSProfile(ref string) (*x509.CertPool, bool) {
if len(srv.ep.inboundMTLSProfiles) == 0 { // nil or empty map
return nil, false
}
pool, ok := srv.ep.inboundMTLSProfiles[ref]
return pool, ok
}

View File

@@ -1,556 +0,0 @@
package entrypoint
import (
"bufio"
"context"
"crypto/tls"
"crypto/x509"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
agentcert "github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/agentpool"
autocert "github.com/yusing/godoxy/internal/autocert/types"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/homepage"
nettypes "github.com/yusing/godoxy/internal/net/types"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/pool"
"github.com/yusing/goutils/task"
)
type fakeHTTPRoute struct {
key string
name string
inboundMTLSProfile string
listenURL *nettypes.URL
routeMiddlewares map[string]types.LabelMap
handler http.HandlerFunc
task *task.Task
}
func newFakeHTTPRoute(t *testing.T, alias, profile string) *fakeHTTPRoute {
return newFakeHTTPRouteAt(t, alias, profile, "https://:1000")
}
func newFakeHTTPRouteAt(t *testing.T, alias, profile, listenURL string) *fakeHTTPRoute {
t.Helper()
return &fakeHTTPRoute{
key: alias,
name: alias,
inboundMTLSProfile: profile,
listenURL: nettypes.MustParseURL(listenURL),
task: task.GetTestTask(t),
}
}
func (r *fakeHTTPRoute) Key() string { return r.key }
func (r *fakeHTTPRoute) Name() string { return r.name }
func (r *fakeHTTPRoute) Start(task.Parent) error { return nil }
func (r *fakeHTTPRoute) Task() *task.Task { return r.task }
func (r *fakeHTTPRoute) Finish(any) {
// no-op: test stub
}
func (r *fakeHTTPRoute) MarshalZerologObject(*zerolog.Event) {
// no-op: test stub
}
func (r *fakeHTTPRoute) ProviderName() string { return "" }
func (r *fakeHTTPRoute) GetProvider() types.RouteProvider { return nil }
func (r *fakeHTTPRoute) ListenURL() *nettypes.URL { return r.listenURL }
func (r *fakeHTTPRoute) TargetURL() *nettypes.URL { return nil }
func (r *fakeHTTPRoute) HealthMonitor() types.HealthMonitor { return nil }
func (r *fakeHTTPRoute) SetHealthMonitor(types.HealthMonitor) {
// no-op: test stub
}
func (r *fakeHTTPRoute) References() []string { return nil }
func (r *fakeHTTPRoute) ShouldExclude() bool { return false }
func (r *fakeHTTPRoute) Started() <-chan struct{} { return nil }
func (r *fakeHTTPRoute) IdlewatcherConfig() *types.IdlewatcherConfig { return nil }
func (r *fakeHTTPRoute) HealthCheckConfig() types.HealthCheckConfig { return types.HealthCheckConfig{} }
func (r *fakeHTTPRoute) LoadBalanceConfig() *types.LoadBalancerConfig {
return nil
}
func (r *fakeHTTPRoute) HomepageItem() homepage.Item { return homepage.Item{} }
func (r *fakeHTTPRoute) DisplayName() string { return r.name }
func (r *fakeHTTPRoute) ContainerInfo() *types.Container {
return nil
}
func (r *fakeHTTPRoute) GetAgent() *agentpool.Agent { return nil }
func (r *fakeHTTPRoute) IsDocker() bool { return false }
func (r *fakeHTTPRoute) IsAgent() bool { return false }
func (r *fakeHTTPRoute) UseLoadBalance() bool { return false }
func (r *fakeHTTPRoute) UseIdleWatcher() bool { return false }
func (r *fakeHTTPRoute) UseHealthCheck() bool { return false }
func (r *fakeHTTPRoute) UseAccessLog() bool { return false }
func (r *fakeHTTPRoute) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if r.handler != nil {
r.handler(w, req)
}
}
func (r *fakeHTTPRoute) InboundMTLSProfileRef() string { return r.inboundMTLSProfile }
func (r *fakeHTTPRoute) RouteMiddlewares() map[string]types.LabelMap { return r.routeMiddlewares }
func newTestHTTPServer(t *testing.T, ep *Entrypoint) *httpServer {
t.Helper()
srv, ok := ep.servers.Load(common.ProxyHTTPAddr)
if ok {
return srv
}
srv = &httpServer{
ep: ep,
addr: common.ProxyHTTPAddr,
routes: pool.New[types.HTTPRoute]("test-http-routes", "test-http-routes"),
}
srv.resetRouteEntrypointOverlays()
ep.servers.Store(common.ProxyHTTPAddr, srv)
return srv
}
func TestMutateServerTLSConfigWithGlobalProfile(t *testing.T) {
ep := NewTestEntrypoint(t, &Config{InboundMTLSProfile: "global"})
srv := newTestHTTPServer(t, ep)
require.NoError(t, ep.SetInboundMTLSProfiles(map[string]types.InboundMTLSProfile{
"global": {UseSystemCAs: true},
}))
base := &tls.Config{MinVersion: tls.VersionTLS12}
mutated := srv.mutateServerTLSConfig(base)
require.Equal(t, tls.RequireAndVerifyClientCert, mutated.ClientAuth)
require.NotNil(t, mutated.ClientCAs)
require.Nil(t, mutated.GetConfigForClient)
}
func TestMutateServerTLSConfigWithoutProfilesKeepsTLSOpen(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
srv := newTestHTTPServer(t, ep)
require.NoError(t, ep.SetInboundMTLSProfiles(nil))
base := &tls.Config{MinVersion: tls.VersionTLS12}
mutated := srv.mutateServerTLSConfig(base)
require.Zero(t, mutated.ClientAuth)
require.Nil(t, mutated.ClientCAs)
require.NotNil(t, mutated.GetConfigForClient)
cfg, err := mutated.GetConfigForClient(&tls.ClientHelloInfo{})
require.NoError(t, err)
require.Zero(t, cfg.ClientAuth)
require.Nil(t, cfg.ClientCAs)
require.Nil(t, cfg.GetConfigForClient)
}
func TestMutateServerTLSConfigWithRouteProfiles(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
ep.SetFindRouteDomains([]string{".example.com"})
srv := newTestHTTPServer(t, ep)
srv.AddRoute(newFakeHTTPRoute(t, "secure-app", "route"))
srv.AddRoute(newFakeHTTPRoute(t, "open-app", ""))
require.NoError(t, ep.SetInboundMTLSProfiles(map[string]types.InboundMTLSProfile{
"route": {UseSystemCAs: true},
}))
base := &tls.Config{MinVersion: tls.VersionTLS12}
mutated := srv.mutateServerTLSConfig(base)
require.Zero(t, mutated.ClientAuth)
require.Nil(t, mutated.ClientCAs)
require.NotNil(t, mutated.GetConfigForClient)
secureCfg, err := mutated.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "secure-app.example.com"})
require.NoError(t, err)
require.Equal(t, tls.RequireAndVerifyClientCert, secureCfg.ClientAuth)
require.NotNil(t, secureCfg.ClientCAs)
require.Nil(t, secureCfg.GetConfigForClient)
openCfg, err := mutated.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "open-app.example.com"})
require.NoError(t, err)
require.Zero(t, openCfg.ClientAuth)
require.Nil(t, openCfg.ClientCAs)
require.Nil(t, openCfg.GetConfigForClient)
unknownCfg, err := mutated.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "unknown.example.com"})
require.NoError(t, err)
require.Zero(t, unknownCfg.ClientAuth)
require.Nil(t, unknownCfg.ClientCAs)
require.Nil(t, unknownCfg.GetConfigForClient)
}
func TestMutateServerTLSConfigFallsBackToRouteProfilesAfterGlobalLookupError(t *testing.T) {
ep := NewTestEntrypoint(t, &Config{InboundMTLSProfile: "missing"})
ep.SetFindRouteDomains([]string{".example.com"})
srv := newTestHTTPServer(t, ep)
srv.AddRoute(newFakeHTTPRoute(t, "secure-app", "route"))
ep.inboundMTLSProfiles = map[string]*x509.CertPool{
"route": x509.NewCertPool(),
}
base := &tls.Config{MinVersion: tls.VersionTLS12}
mutated := srv.mutateServerTLSConfig(base)
require.NotNil(t, mutated.GetConfigForClient)
secureCfg, err := mutated.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "secure-app.example.com"})
require.NoError(t, err)
require.Equal(t, tls.RequireAndVerifyClientCert, secureCfg.ClientAuth)
require.NotNil(t, secureCfg.ClientCAs)
require.Nil(t, secureCfg.GetConfigForClient)
openCfg, err := mutated.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "open-app.example.com"})
require.NoError(t, err)
require.Zero(t, openCfg.ClientAuth)
require.Nil(t, openCfg.ClientCAs)
require.Nil(t, openCfg.GetConfigForClient)
}
func TestSetInboundMTLSProfilesRejectsUnknownGlobalProfile(t *testing.T) {
ep := NewTestEntrypoint(t, &Config{InboundMTLSProfile: "missing"})
err := ep.SetInboundMTLSProfiles(map[string]types.InboundMTLSProfile{
"known": {UseSystemCAs: true},
})
require.Error(t, err)
require.ErrorContains(t, err, `entrypoint inbound mTLS profile "missing" not found`)
}
func TestSetInboundMTLSProfilesRejectsBadCAFile(t *testing.T) {
ep := NewTestEntrypoint(t, &Config{InboundMTLSProfile: "broken"})
err := ep.SetInboundMTLSProfiles(map[string]types.InboundMTLSProfile{
"broken": {CAFiles: []string{filepath.Join(t.TempDir(), "missing.pem")}},
})
require.Error(t, err)
require.ErrorContains(t, err, "missing.pem")
}
func TestCompileInboundMTLSProfilesReturnsNilMapOnError(t *testing.T) {
compiled, err := compileInboundMTLSProfiles(map[string]types.InboundMTLSProfile{
"ok": {UseSystemCAs: true},
"bad": {CAFiles: []string{filepath.Join(t.TempDir(), "missing.pem")}},
})
require.Nil(t, compiled)
require.Error(t, err)
require.ErrorContains(t, err, "missing.pem")
}
func TestMutateServerTLSConfigRejectsUnknownRouteProfile(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
ep.SetFindRouteDomains([]string{".example.com"})
srv := newTestHTTPServer(t, ep)
srv.AddRoute(newFakeHTTPRoute(t, "secure-app", "missing"))
base := &tls.Config{MinVersion: tls.VersionTLS12}
mutated := srv.mutateServerTLSConfig(base)
_, err := mutated.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "secure-app.example.com"})
require.Error(t, err)
require.ErrorContains(t, err, `route "secure-app" inbound mTLS profile "missing" not found`)
}
func TestResolveRequestRouteRejectsUnknownRouteProfile(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
ep.SetFindRouteDomains([]string{".example.com"})
srv := newTestHTTPServer(t, ep)
srv.AddRoute(newFakeHTTPRoute(t, "secure-app", "missing"))
req := httptest.NewRequest(http.MethodGet, "https://secure-app.example.com", nil)
req.Host = "secure-app.example.com"
req.TLS = &tls.ConnectionState{ServerName: "secure-app.example.com"}
route, err := srv.resolveRequestRoute(req)
require.Nil(t, route)
require.Error(t, err)
require.ErrorContains(t, err, `route "secure-app" inbound mTLS profile "missing" not found`)
}
func TestResolveRequestRouteLeavesOpenAndUnknownHostsUnchanged(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
ep.SetFindRouteDomains([]string{".example.com"})
srv := newTestHTTPServer(t, ep)
srv.AddRoute(newFakeHTTPRoute(t, "secure-app", "route"))
srv.AddRoute(newFakeHTTPRoute(t, "open-app", ""))
require.NoError(t, ep.SetInboundMTLSProfiles(map[string]types.InboundMTLSProfile{
"route": {UseSystemCAs: true},
}))
t.Run("open host stays open", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "https://open-app.example.com", nil)
req.Host = "open-app.example.com"
req.TLS = &tls.ConnectionState{ServerName: "open-app.example.com"}
route, err := srv.resolveRequestRoute(req)
require.NoError(t, err)
require.NotNil(t, route)
require.Equal(t, "open-app", route.Name())
})
t.Run("unknown host falls through", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "https://unknown.example.com", nil)
req.Host = "unknown.example.com"
req.TLS = &tls.ConnectionState{ServerName: "unknown.example.com"}
route, err := srv.resolveRequestRoute(req)
require.NoError(t, err)
require.Nil(t, route)
})
}
func TestInboundMTLSGlobalHandshake(t *testing.T) {
ca, srv, client, err := agentcert.NewAgent()
require.NoError(t, err)
serverCert, err := srv.ToTLSCert()
require.NoError(t, err)
clientCert, err := client.ToTLSCert()
require.NoError(t, err)
caPath := writeTempFile(t, "ca.pem", ca.Cert)
provider := &staticCertProvider{cert: serverCert}
ep := NewTestEntrypoint(t, &Config{InboundMTLSProfile: "global"})
t.Cleanup(func() {
closeTestServers(t, ep)
})
autocert.SetCtx(task.GetTestTask(t), provider)
require.NoError(t, ep.SetInboundMTLSProfiles(map[string]types.InboundMTLSProfile{
"global": {CAFiles: []string{caPath}},
}))
listener, releaseListener := reserveTCPAddr(t)
listenAddr := listener.Addr().String()
addHTTPRouteAt(t, ep, "app1", "", listenAddr, listener)
releaseListener()
t.Run("trusted client succeeds", func(t *testing.T) {
resp, err := doHTTPSRequest(listenAddr, "app1.example.com", &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{*clientCert},
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
})
t.Run("missing client cert fails handshake", func(t *testing.T) {
_, err := doHTTPSRequest(listenAddr, "app1.example.com", &tls.Config{
InsecureSkipVerify: true,
})
require.Error(t, err)
})
t.Run("wrong client cert fails handshake", func(t *testing.T) {
_, _, badClient, err := agentcert.NewAgent()
require.NoError(t, err)
badClientCert, err := badClient.ToTLSCert()
require.NoError(t, err)
_, err = doHTTPSRequest(listenAddr, "app1.example.com", &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{*badClientCert},
})
require.Error(t, err)
})
}
func TestInboundMTLSRouteScopedHandshake(t *testing.T) {
ca, srv, client, err := agentcert.NewAgent()
require.NoError(t, err)
serverCert, err := srv.ToTLSCert()
require.NoError(t, err)
clientCert, err := client.ToTLSCert()
require.NoError(t, err)
caPath := writeTempFile(t, "ca.pem", ca.Cert)
provider := &staticCertProvider{cert: serverCert}
ep := NewTestEntrypoint(t, nil)
t.Cleanup(func() {
closeTestServers(t, ep)
})
ep.SetFindRouteDomains([]string{".example.com"})
autocert.SetCtx(task.GetTestTask(t), provider)
require.NoError(t, ep.SetInboundMTLSProfiles(map[string]types.InboundMTLSProfile{
"route": {CAFiles: []string{caPath}},
}))
listener, releaseListener := reserveTCPAddr(t)
listenAddr := listener.Addr().String()
addHTTPRouteAt(t, ep, "secure-app", "route", listenAddr, listener)
releaseListener()
addHTTPRouteAt(t, ep, "open-app", "", listenAddr, nil)
t.Run("secure route requires client cert when sni matches", func(t *testing.T) {
_, err := doHTTPSRequest(listenAddr, "secure-app.example.com", &tls.Config{
InsecureSkipVerify: true,
})
require.Error(t, err)
})
t.Run("secure route accepts trusted client cert", func(t *testing.T) {
resp, err := doHTTPSRequest(listenAddr, "secure-app.example.com", &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{*clientCert},
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
})
t.Run("open route without client cert succeeds", func(t *testing.T) {
resp, err := doHTTPSRequest(listenAddr, "open-app.example.com", &tls.Config{
InsecureSkipVerify: true,
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
})
t.Run("secure route rejects requests without sni", func(t *testing.T) {
resp, tlsConn, err := doHTTPSRequestWithServerName(listenAddr, "secure-app.example.com", "", &tls.Config{
InsecureSkipVerify: true,
})
require.NoError(t, err)
defer func() { _ = tlsConn.Close() }()
defer func() { _ = resp.Body.Close() }()
require.Equal(t, http.StatusMisdirectedRequest, resp.StatusCode)
})
t.Run("secure route rejects host and sni mismatch without cert", func(t *testing.T) {
resp, tlsConn, err := doHTTPSRequestWithServerName(listenAddr, "secure-app.example.com", "open-app.example.com", &tls.Config{
InsecureSkipVerify: true,
})
require.NoError(t, err)
defer func() { _ = tlsConn.Close() }()
defer func() { _ = resp.Body.Close() }()
require.Equal(t, http.StatusMisdirectedRequest, resp.StatusCode)
})
t.Run("open route rejects host and sni mismatch when sni selects secure route", func(t *testing.T) {
resp, tlsConn, err := doHTTPSRequestWithServerName(listenAddr, "open-app.example.com", "secure-app.example.com", &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{*clientCert},
})
require.NoError(t, err)
defer func() { _ = tlsConn.Close() }()
defer func() { _ = resp.Body.Close() }()
require.Equal(t, http.StatusMisdirectedRequest, resp.StatusCode)
})
}
func addHTTPRouteAt(t *testing.T, ep *Entrypoint, alias, profile, listenAddr string, listener net.Listener) {
t.Helper()
route := newFakeHTTPRouteAt(t, alias, profile, "https://"+listenAddr)
if listener == nil {
require.NoError(t, ep.StartAddRoute(route))
return
}
require.NoError(t, ep.addHTTPRouteWithListener(route, listenAddr, HTTPProtoHTTPS, listener))
}
func closeTestServers(t *testing.T, ep *Entrypoint) {
t.Helper()
for _, srv := range ep.servers.Range {
srv.Close()
}
}
func reserveTCPAddr(t *testing.T) (net.Listener, func()) {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
owned := true
t.Cleanup(func() {
if owned {
_ = ln.Close()
}
})
return ln, func() {
owned = false
}
}
func writeTempFile(t *testing.T, name string, data []byte) string {
t.Helper()
path := filepath.Join(t.TempDir(), name)
require.NoError(t, os.WriteFile(path, data, 0o600))
return path
}
func doHTTPSRequest(addr, host string, tlsConfig *tls.Config) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, "https://"+addr, nil)
if err != nil {
return nil, err
}
req.Host = host
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: cloneTLSConfigWithServerName(tlsConfig, host),
},
}
return client.Do(req)
}
// doHTTPSRequestWithServerName sends GET https://addr/ with HTTP Host set to host and TLS
// ServerName set to serverName (SNI may differ from Host). The returned connection stays open
// until the caller closes it after finishing with resp (typically close resp.Body first, then
// the tls connection).
func doHTTPSRequestWithServerName(addr, host, serverName string, tlsConfig *tls.Config) (*http.Response, io.Closer, error) {
conn, err := tls.Dial("tcp", addr, cloneTLSConfigWithServerName(tlsConfig, serverName))
if err != nil {
return nil, nil, err
}
req, err := http.NewRequest(http.MethodGet, "https://"+addr, nil)
if err != nil {
_ = conn.Close()
return nil, nil, err
}
req.Host = host
if err := req.Write(conn); err != nil {
_ = conn.Close()
return nil, nil, err
}
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
if err != nil {
_ = conn.Close()
return nil, nil, err
}
return resp, conn, nil
}
func cloneTLSConfigWithServerName(cfg *tls.Config, serverName string) *tls.Config {
if cfg == nil {
cfg = &tls.Config{}
}
cloned := cfg.Clone()
cloned.ServerName = serverName
return cloned
}
type staticCertProvider struct {
cert *tls.Certificate
}
func (p *staticCertProvider) GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return p.cert, nil
}
func (p *staticCertProvider) GetCertInfos() ([]autocert.CertInfo, error) { return nil, nil }
func (p *staticCertProvider) ScheduleRenewalAll(task.Parent) {
// no-op: test stub
}
func (p *staticCertProvider) ObtainCertAll() error { return nil }
func (p *staticCertProvider) ForceExpiryAll() bool { return false }
func (p *staticCertProvider) WaitRenewalDone(context.Context) bool { return true }

View File

@@ -113,14 +113,10 @@ func (ep *Entrypoint) AddHTTPRoute(route types.HTTPRoute) error {
}
func (ep *Entrypoint) addHTTPRoute(route types.HTTPRoute, addr string, proto HTTPProto) error {
return ep.addHTTPRouteWithListener(route, addr, proto, nil)
}
func (ep *Entrypoint) addHTTPRouteWithListener(route types.HTTPRoute, addr string, proto HTTPProto, listener net.Listener) error {
var err error
srv, _ := ep.servers.LoadOrCompute(addr, func() (newSrv *httpServer, cancel bool) {
newSrv = newHTTPServer(ep)
err = newSrv.listen(addr, proto, listener)
err = newSrv.Listen(addr, proto)
cancel = err != nil
return
})

View File

@@ -6,7 +6,7 @@ import (
"net/http"
"time"
"encoding/json"
"github.com/bytedance/sonic"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/yusing/godoxy/internal/docker"
@@ -108,7 +108,7 @@ func interceptDockerInspectResponse(resp *http.Response) (intercepted bool, err
}
var state container.State
err = json.Unmarshal(body, &state)
err = sonic.Unmarshal(body, &state)
release(body)
if err != nil {
return false, err

View File

@@ -7,7 +7,7 @@ import (
"strings"
"time"
"encoding/json"
"github.com/bytedance/sonic"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
@@ -55,7 +55,7 @@ func init() {
func InitCache() {
m := make(IconMap)
err := serialization.LoadFileIfExist(common.IconListCachePath, &m, json.Unmarshal)
err := serialization.LoadFileIfExist(common.IconListCachePath, &m, sonic.Unmarshal)
switch {
case err != nil:
// backward compatible
@@ -63,13 +63,13 @@ func InitCache() {
Icons IconMap
LastUpdate time.Time
}{}
err = serialization.LoadFileIfExist(common.IconListCachePath, &oldFormat, json.Unmarshal)
err = serialization.LoadFileIfExist(common.IconListCachePath, &oldFormat, sonic.Unmarshal)
if err != nil {
log.Error().Err(err).Msg("failed to load icons")
} else {
m = oldFormat.Icons
// store it to disk immediately
_ = serialization.SaveFile(common.IconListCachePath, &m, 0o644, json.Marshal)
_ = serialization.SaveFile(common.IconListCachePath, &m, 0o644, sonic.Marshal)
}
case len(m) > 0:
log.Info().
@@ -85,7 +85,7 @@ func InitCache() {
task.OnProgramExit("save_icons_cache", func() {
icons := iconsCache.Load()
_ = serialization.SaveFile(common.IconListCachePath, &icons, 0o644, json.Marshal)
_ = serialization.SaveFile(common.IconListCachePath, &icons, 0o644, sonic.Marshal)
})
go backgroundUpdateIcons()
@@ -106,7 +106,7 @@ func backgroundUpdateIcons() {
// swap old cache with new cache
iconsCache.Store(newCache)
// save it to disk
err := serialization.SaveFile(common.IconListCachePath, &newCache, 0o644, json.Marshal)
err := serialization.SaveFile(common.IconListCachePath, &newCache, 0o644, sonic.Marshal)
if err != nil {
log.Warn().Err(err).Msg("failed to save icons")
}
@@ -286,7 +286,7 @@ func UpdateWalkxCodeIcons(m IconMap) error {
}
data := make(map[string][]string)
err = json.Unmarshal(body, &data)
err = sonic.Unmarshal(body, &data)
release(body)
if err != nil {
return err
@@ -365,7 +365,7 @@ func UpdateSelfhstIcons(m IconMap) error {
}
data := make([]SelfhStIcon, 0)
err = json.Unmarshal(body, &data) //nolint:musttag
err = sonic.Unmarshal(body, &data) //nolint:musttag
release(body)
if err != nil {
return err

View File

@@ -8,7 +8,7 @@ import (
"net/http"
"net/url"
"encoding/json"
"github.com/bytedance/sonic"
"github.com/yusing/godoxy/internal/homepage/widgets"
strutils "github.com/yusing/goutils/strings"
)
@@ -71,7 +71,7 @@ func jsonRequest[T any](ctx context.Context, client *Client, endpoint string, qu
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&result)
err = sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return result, err
}

View File

@@ -6,7 +6,7 @@ import (
"strconv"
"time"
"encoding/json"
"github.com/bytedance/sonic"
)
const endpointLogs = "/api/v2/log/main"
@@ -45,7 +45,7 @@ func (l *LogEntry) Level() string {
}
func (l *LogEntry) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
return sonic.Marshal(map[string]any{
"id": l.ID,
"timestamp": l.Timestamp,
"level": l.Level(),

View File

@@ -4,7 +4,7 @@ import (
"iter"
"strconv"
"encoding/json"
"github.com/bytedance/sonic"
strutils "github.com/yusing/goutils/strings"
)
@@ -14,7 +14,7 @@ type watcherDebug struct {
func (w watcherDebug) MarshalJSON() ([]byte, error) {
state := w.state.Load()
return json.Marshal(map[string]any{
return sonic.Marshal(map[string]any{
"name": w.Name(),
"state": map[string]string{
"status": string(state.status),

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"io"
"encoding/json"
"github.com/bytedance/sonic"
gevents "github.com/yusing/goutils/events"
)
@@ -26,7 +26,7 @@ const (
)
func writeSSE(w io.Writer, v any) error {
data, err := json.Marshal(v)
data, err := sonic.Marshal(v)
if err != nil {
return err
}

View File

@@ -16,6 +16,21 @@ import (
_ "unsafe"
)
type ForceCacheControl struct {
expires string
http.ResponseWriter
}
func (f *ForceCacheControl) WriteHeader(code int) {
f.ResponseWriter.Header().Set("Cache-Control", "must-revalidate")
f.ResponseWriter.Header().Set("Expires", f.expires)
f.ResponseWriter.WriteHeader(code)
}
func (f *ForceCacheControl) Unwrap() http.ResponseWriter {
return f.ResponseWriter
}
// ServeHTTP implements http.Handler.
func (w *Watcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
shouldNext := w.wakeFromHTTP(rw, r)
@@ -26,14 +41,15 @@ func (w *Watcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
case <-r.Context().Done():
return
default:
w.rp.ServeHTTP(rw, r)
f := &ForceCacheControl{expires: w.expires().Format(http.TimeFormat), ResponseWriter: rw}
w.rp.ServeHTTP(f, r)
}
}
func (w *Watcher) handleWakeEventsSSE(rw http.ResponseWriter, r *http.Request) {
// Set SSE headers
rw.Header().Set("Content-Type", "text/event-stream")
setNoStoreHeaders(rw.Header())
rw.Header().Set("Cache-Control", "no-cache")
rw.Header().Set("Connection", "keep-alive")
rw.Header().Set("Access-Control-Allow-Origin", "*")
rw.Header().Set("Access-Control-Allow-Headers", "Cache-Control")
@@ -111,15 +127,12 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
fmt.Fprint(rw, err)
return false
}
setNoStoreHeaders(rw.Header())
serveStaticContent(rw, result.StatusCode, result.ContentType(), result.Icon)
return false
case idlewatcher.LoadingPageCSSPath:
setNoStoreHeaders(rw.Header())
serveStaticContent(rw, http.StatusOK, "text/css", cssBytes)
return false
case idlewatcher.LoadingPageJSPath:
setNoStoreHeaders(rw.Header())
serveStaticContent(rw, http.StatusOK, "application/javascript", jsBytes)
return false
case idlewatcher.WakeEventsPath:
@@ -165,7 +178,9 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
// Send a loading response to the client
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
setNoStoreHeaders(rw.Header())
rw.Header().Set("Cache-Control", "no-cache")
rw.Header().Add("Cache-Control", "no-store")
rw.Header().Add("Cache-Control", "must-revalidate")
rw.Header().Add("Connection", "close")
_ = w.writeLoadingPage(rw)
return false

View File

@@ -1,112 +0,0 @@
package idlewatcher
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/require"
idlewatchertypes "github.com/yusing/godoxy/internal/idlewatcher/types"
"github.com/yusing/godoxy/internal/types"
gevents "github.com/yusing/goutils/events"
"github.com/yusing/goutils/http/reverseproxy"
)
func TestWriteLoadingPageDisablesCaching(t *testing.T) {
w := newTestWatcher(t)
rec := httptest.NewRecorder()
err := w.writeLoadingPage(rec)
require.NoError(t, err)
require.Equal(t, "no-store, no-cache, must-revalidate, max-age=0", rec.Header().Get("Cache-Control"))
require.Equal(t, "no-cache", rec.Header().Get("Pragma"))
require.Equal(t, "0", rec.Header().Get("Expires"))
}
func TestServeHTTPLoadingEndpointsDisableCaching(t *testing.T) {
for _, path := range []string{
idlewatchertypes.LoadingPageCSSPath,
idlewatchertypes.LoadingPageJSPath,
idlewatchertypes.WakeEventsPath,
} {
t.Run(path, func(t *testing.T) {
w := newTestWatcher(t)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "http://example.com"+path, nil)
if path == idlewatchertypes.WakeEventsPath {
ctx, cancel := context.WithCancel(req.Context())
cancel()
req = req.WithContext(ctx)
}
w.ServeHTTP(rec, req)
require.Equal(t, "no-store, no-cache, must-revalidate, max-age=0", rec.Header().Get("Cache-Control"))
require.Equal(t, "no-cache", rec.Header().Get("Pragma"))
require.Equal(t, "0", rec.Header().Get("Expires"))
})
}
}
func TestServeHTTPReadyProxyPreservesUpstreamCacheHeaders(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
rw.Header().Set("Cache-Control", "public, max-age=600")
rw.Header().Set("Expires", "Tue, 21 Apr 2026 00:00:00 GMT")
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte("ok"))
}))
defer upstream.Close()
targetURL, err := url.Parse(upstream.URL)
require.NoError(t, err)
w := newTestWatcher(t)
w.rp = reverseproxy.NewReverseProxy("idlewatcher-test", targetURL, upstream.Client().Transport)
w.state.Store(&containerState{
status: idlewatchertypes.ContainerStatusRunning,
ready: true,
})
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
w.ServeHTTP(rec, req)
require.Equal(t, "public, max-age=600", rec.Header().Get("Cache-Control"))
require.Equal(t, "Tue, 21 Apr 2026 00:00:00 GMT", rec.Header().Get("Expires"))
require.Equal(t, http.StatusOK, rec.Code)
require.Equal(t, "ok", rec.Body.String())
}
func newTestWatcher(t *testing.T) *Watcher {
t.Helper()
ticker := time.NewTicker(time.Hour)
t.Cleanup(ticker.Stop)
w := &Watcher{
cfg: &types.IdlewatcherConfig{
IdlewatcherProviderConfig: types.IdlewatcherProviderConfig{
Docker: &types.DockerConfig{
ContainerName: "test-container",
},
},
IdlewatcherConfigBase: types.IdlewatcherConfigBase{
IdleTimeout: time.Hour,
WakeTimeout: time.Second,
},
},
idleTicker: ticker,
readyNotifyCh: make(chan struct{}, 1),
events: gevents.NewHistory(),
}
w.lastReset.Store(time.Now())
w.state.Store(&containerState{
status: idlewatchertypes.ContainerStatusStopped,
})
return w
}

View File

@@ -48,17 +48,10 @@ window.onload = async () => {
* @param {string} type - Console line type (e.g. ready, error, or WakeEventType)
* @param {string} message
* @param {string} timestamp - ISO timestamp string
* @param {EventLevel} [level]
*/
function addConsoleLine(type, message, timestamp, level) {
const validLevels = ["debug", "info", "warn", "error"];
const lvl = validLevels.includes(level)
? level
: type === "error"
? "error"
: "info";
function addConsoleLine(type, message, timestamp) {
const line = document.createElement("div");
line.className = `console-line ${type} level-${lvl}`;
line.className = `console-line ${type}`;
const timestampEl = document.createElement("span");
timestampEl.className = "console-timestamp";
@@ -120,25 +113,20 @@ window.onload = async () => {
ready = true;
// Container is ready, hide loading dots and refresh
loadingDotsEl.style.display = "none";
addConsoleLine(
type,
"Container is ready, refreshing...",
timestamp,
evt.level,
);
addConsoleLine(type, "Container is ready, refreshing...", timestamp);
setTimeout(() => {
window.location.reload();
}, 200);
} else if (type === "error" || evt.level === "error") {
// Show error message and hide loading dots
const errorMessage = payload.error || payload.message || "Unknown error";
addConsoleLine(type, errorMessage, timestamp, evt.level);
addConsoleLine(type, errorMessage, timestamp);
loadingDotsEl.style.display = "none";
eventSource.close();
} else {
// Show other message types
const message = payload.message || "";
addConsoleLine(type, message, timestamp, evt.level);
const message = payload.message;
addConsoleLine(type, message, timestamp);
}
};

View File

@@ -1,249 +0,0 @@
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _async_to_generator(fn) {
return function () {
var self = this,
args = arguments;
return new Promise(function (resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
};
}
function _ts_generator(thisArg, body) {
var f,
y,
t,
_ = {
label: 0,
sent: function () {
if (t[0] & 1) throw t[1];
return t[1];
},
trys: [],
ops: [],
},
g = Object.create(
(typeof Iterator === "function" ? Iterator : Object).prototype,
),
d = Object.defineProperty;
return (
d(g, "next", { value: verb(0) }),
d(g, "throw", { value: verb(1) }),
d(g, "return", { value: verb(2) }),
typeof Symbol === "function" &&
d(g, Symbol.iterator, {
value: function () {
return this;
},
}),
g
);
function verb(n) {
return function (v) {
return step([n, v]);
};
}
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while ((g && ((g = 0), op[0] && (_ = 0)), _))
try {
if (
((f = 1),
y &&
(t =
op[0] & 2
? y["return"]
: op[0]
? y["throw"] || ((t = y["return"]) && t.call(y), 0)
: y.next) &&
!(t = t.call(y, op[1])).done)
)
return t;
if (((y = 0), t)) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0:
case 1:
t = op;
break;
case 4:
_.label++;
return { value: op[1], done: false };
case 5:
_.label++;
y = op[1];
op = [0];
continue;
case 7:
op = _.ops.pop();
_.trys.pop();
continue;
default:
if (
!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) &&
(op[0] === 6 || op[0] === 2)
) {
_ = 0;
continue;
}
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
_.label = op[1];
break;
}
if (op[0] === 6 && _.label < t[1]) {
_.label = t[1];
t = op;
break;
}
if (t && _.label < t[2]) {
_.label = t[2];
_.ops.push(op);
break;
}
if (t[2]) _.ops.pop();
_.trys.pop();
continue;
}
op = body.call(thisArg, _);
} catch (e) {
op = [6, e];
y = 0;
} finally {
f = t = 0;
}
if (op[0] & 5) throw op[1];
return { value: op[0] ? op[1] : void 0, done: true };
}
}
var ready = false;
window.onload = function () {
return _async_to_generator(function () {
var consoleEl, loadingDotsEl, eventSource;
function formatTimestamp(timestamp) {
var date = new Date(timestamp);
return date.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
fractionalSecondDigits: 3,
});
}
function addConsoleLine(type, message, timestamp, level) {
var validLevels = ["debug", "info", "warn", "error"];
var lvl = validLevels.includes(level)
? level
: type === "error"
? "error"
: "info";
var line = document.createElement("div");
line.className = "console-line ".concat(type, " level-").concat(lvl);
var timestampEl = document.createElement("span");
timestampEl.className = "console-timestamp";
timestampEl.textContent = formatTimestamp(timestamp);
var messageEl = document.createElement("span");
messageEl.className = "console-message";
messageEl.textContent = message;
line.appendChild(timestampEl);
line.appendChild(messageEl);
consoleEl.appendChild(line);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
return _ts_generator(this, function (_state) {
consoleEl = document.getElementById("console");
loadingDotsEl = document.getElementById("loading-dots");
if (!consoleEl || !loadingDotsEl) {
console.error("Required DOM elements not found");
return [2];
}
if (typeof wakeEventsPath === "undefined" || !wakeEventsPath) {
addConsoleLine(
"error",
"Configuration error: wakeEventsPath not defined",
new Date().toISOString(),
);
loadingDotsEl.style.display = "none";
return [2];
}
if (typeof EventSource === "undefined") {
addConsoleLine(
"error",
"Browser does not support Server-Sent Events",
new Date().toISOString(),
);
loadingDotsEl.style.display = "none";
return [2];
}
eventSource = new EventSource(wakeEventsPath);
eventSource.onmessage = function (event) {
var evt;
try {
evt = JSON.parse(event.data);
} catch (unused) {
addConsoleLine(
"error",
"Invalid event data: ".concat(event.data),
new Date().toISOString(),
);
return;
}
var payload = evt.data || {};
var type = evt.action;
var timestamp = evt.timestamp;
if (type === "ready") {
ready = true;
loadingDotsEl.style.display = "none";
addConsoleLine(
type,
"Container is ready, refreshing...",
timestamp,
evt.level,
);
setTimeout(function () {
window.location.reload();
}, 200);
} else if (type === "error" || evt.level === "error") {
var errorMessage =
payload.error || payload.message || "Unknown error";
addConsoleLine(type, errorMessage, timestamp, evt.level);
loadingDotsEl.style.display = "none";
eventSource.close();
} else {
var message = payload.message || "";
addConsoleLine(type, message, timestamp, evt.level);
}
};
eventSource.onerror = function () {
if (ready) {
return;
}
addConsoleLine(
"error",
"Connection lost. Please refresh the page.",
new Date().toISOString(),
);
loadingDotsEl.style.display = "none";
eventSource.close();
};
return [2];
});
})();
};

View File

@@ -1,12 +1,17 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<title>{{.Title}}</title>
<link rel="stylesheet" href="{{.LoadingPageCSSPath}}" />
<link rel="icon" href="{{.FavIconPath}}" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&display=swap"
rel="stylesheet"
/>
</head>
<body>
<script>
@@ -15,8 +20,8 @@
<script src="{{.LoadingPageJSPath}}" defer></script>
<div class="container">
<!-- icon handled by waker_http -->
<img class="logo" src="{{.FavIconPath}}" alt="" />
<div id="loading-dots" class="loading-dots" aria-hidden="true">
<img class="logo" src="{{.FavIconPath}}" />
<div id="loading-dots" class="loading-dots">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>

View File

@@ -1,26 +0,0 @@
<!doctype html>
<html lang="en">
<meta charset="UTF-8" /><meta
name="viewport"
content="width=device-width,initial-scale=1.0"
/><meta name="color-scheme" content="light dark" /><title>{{.Title}}</title
><link rel="stylesheet" href="{{.LoadingPageCSSPath}}" /><link
rel="icon"
href="{{.FavIconPath}}"
/>
<body>
<script>
const wakeEventsPath = "{{.WakeEventsPath}}";
</script>
<script src="{{.LoadingPageJSPath}}" defer></script>
<div class="container">
<img class="logo" src="{{.FavIconPath}}" alt="" />
<div id="loading-dots" class="loading-dots" aria-hidden="true">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<div id="console" class="console"></div>
</div>
</body>
</html>

View File

@@ -1,283 +1,194 @@
/* size variables */
:root {
color-scheme: light;
--fg: #141413;
--fg-secondary: #52514e;
--bg: #fafaf9;
--surface: #ffffff;
--rule: #292826;
--border: color-mix(in srgb, var(--rule) 12%, var(--bg));
--text: var(--fg);
--muted: var(--fg-secondary);
--faint: color-mix(in srgb, var(--rule) 9%, transparent);
--console-bg: #f5f5f4;
--msg-error: #b91c1c;
--text-emphasis: color-mix(in srgb, var(--fg) 92%, var(--bg) 8%);
--dot-size: 6px;
--logo-size: 72px;
--radius: 10px;
--font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", sans-serif;
--font-mono:
ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
--accent-starting: #52525b;
--accent-waking-dep: #0369a1;
--accent-dep-ready: #15803d;
--accent-container-woke: #047857;
--accent-waiting-ready: #b45309;
--accent-ready: #166534;
--accent-error: #dc2626;
--level-debug: color-mix(in srgb, var(--fg-secondary) 18%, transparent);
--level-info: transparent;
--level-warn: color-mix(in srgb, #b45309 16%, transparent);
--level-error: color-mix(in srgb, #dc2626 12%, transparent);
--dot-size: 12px;
--logo-size: 100px;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--fg: #ecebe8;
--fg-secondary: #9c9a94;
--bg: #0c0c0b;
--surface: #111113;
--rule: #5c5954;
--border: rgba(255, 255, 255, 0.06);
--text: var(--fg);
--muted: var(--fg-secondary);
--faint: rgba(255, 255, 255, 0.08);
--console-bg: #0a0a0b;
--msg-error: #fecaca;
--text-emphasis: rgba(255, 255, 255, 0.95);
--accent-starting: #a1a1aa;
--accent-waking-dep: #38bdf8;
--accent-dep-ready: #4ade80;
--accent-container-woke: #34d399;
--accent-waiting-ready: #fbbf24;
--accent-ready: #86efac;
--accent-error: #f87171;
--level-debug: rgba(161, 161, 170, 0.35);
--level-info: transparent;
--level-warn: rgba(251, 191, 36, 0.2);
--level-error: rgba(248, 113, 113, 0.12);
}
}
*,
*::before,
*::after {
/* Global Styles */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
background-color: var(--bg);
-webkit-text-size-adjust: 100%;
}
body {
font-family: var(--font-sans);
font-size: 15px;
line-height: 1.45;
color: var(--text);
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-size: 16px;
line-height: 1.5;
color: #f8f9fa;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #121212 0%, #1e1e1e 100%);
}
/* Container */
.container {
display: flex;
flex-direction: column;
gap: 32px;
align-items: center;
justify-content: center;
padding: clamp(1rem, 4vw, 1.5rem);
background: var(--bg);
}
@media (prefers-color-scheme: dark) {
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
padding: 32px;
border-radius: 16px;
background-color: rgba(30, 30, 30, 0.9);
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(12px);
max-width: 90%;
transition: all 0.3s ease;
min-width: 600px;
min-height: 400px;
}
@media (max-width: 768px) {
.container {
width: 100%;
max-width: 520px;
display: flex;
flex-direction: column;
align-items: center;
gap: 28px;
padding: 28px 24px 24px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.logo {
width: var(--logo-size);
height: var(--logo-size);
opacity: 0.92;
min-width: auto;
max-width: 90%;
}
}
/* Spinner Styles */
.loading-dots {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 10px;
align-items: center;
gap: 8px;
}
.dot {
width: var(--dot-size);
height: var(--dot-size);
background-color: #66d9ef;
border-radius: 50%;
background: var(--muted);
animation: pulse 1.2s ease-in-out infinite;
animation: bounce 1.3s infinite ease-in-out;
}
.dot:nth-child(1) {
animation-delay: 0ms;
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: 160ms;
animation-delay: -0.16s;
}
.dot:nth-child(3) {
animation-delay: 320ms;
}
@keyframes pulse {
@keyframes bounce {
0%,
80%,
100% {
opacity: 0.25;
transform: scale(0.92);
transform: translateY(0);
}
50% {
opacity: 1;
transform: scale(1);
40% {
transform: translateY(-10px);
}
}
/* Message Styles */
.message {
font-size: 18px;
font-weight: 500;
text-align: left;
color: #f8f9fa;
max-width: 100%;
letter-spacing: 0.3px;
}
/* Console Styles */
.console {
width: 100%;
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
letter-spacing: -0.01em;
background: var(--console-bg);
border: 1px solid var(--border);
border-radius: calc(var(--radius) - 2px);
padding: 10px 0;
min-height: 180px;
max-height: 280px;
font-family: "Fira Code", "Consolas", "Monaco", "Courier New", monospace;
font-size: 14px;
background-color: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 16px;
color: #e0e0e0;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--faint) transparent;
max-height: 300px;
min-height: 200px;
width: 100%;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
}
.console-line {
display: grid;
grid-template-columns: 5.5rem 1fr;
column-gap: 12px;
align-items: baseline;
padding: 6px 12px 6px 14px;
border-inline-start: 2px solid var(--line-accent, var(--accent-starting));
background: linear-gradient(
90deg,
var(--level-bg, transparent) 0%,
transparent 48px
);
border-bottom: 1px solid var(--faint);
display: flex;
align-items: flex-start;
margin-bottom: 4px;
padding: 2px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.console-line:last-child {
border-bottom: none;
}
.console-line.level-debug {
--level-bg: var(--level-debug);
}
.console-line.level-info {
--level-bg: var(--level-info);
}
.console-line.level-warn {
--level-bg: var(--level-warn);
}
.console-line.level-error {
--level-bg: var(--level-error);
border-inline-start-width: 3px;
margin-bottom: 0;
}
.console-timestamp {
color: var(--muted);
font-variant-numeric: tabular-nums;
white-space: nowrap;
color: #66d9ef;
margin-right: 12px;
font-weight: 500;
flex-shrink: 0;
min-width: 80px;
}
.console-message {
color: var(--text);
flex: 1;
word-break: break-word;
white-space: pre-wrap;
}
.console-line.starting {
--line-accent: var(--accent-starting);
.console-line.starting .console-message {
color: #f9f871;
}
.console-line.waking_dep {
--line-accent: var(--accent-waking-dep);
.console-line.waking_dep .console-message {
color: #66d9ef;
}
.console-line.dep_ready {
--line-accent: var(--accent-dep-ready);
.console-line.dep_ready .console-message {
color: #a6e22e;
}
.console-line.container_woke {
--line-accent: var(--accent-container-woke);
.console-line.container_woke .console-message {
color: #a6e22e;
}
.console-line.waiting_ready {
--line-accent: var(--accent-waiting-ready);
}
.console-line.ready {
--line-accent: var(--accent-ready);
.console-line.waiting_ready .console-message {
color: #fd971f;
}
.console-line.ready .console-message {
font-weight: 600;
color: var(--text-emphasis);
}
.console-line.error {
--line-accent: var(--accent-error);
color: #a6e22e;
font-weight: bold;
}
.console-line.error .console-message {
color: var(--msg-error);
font-weight: 500;
color: #f92672;
font-weight: bold;
}
/* Loading dots in console */
.console-loading {
display: inline-flex;
display: flex;
align-items: center;
gap: 3px;
margin-inline-start: 6px;
vertical-align: middle;
gap: 4px;
margin-left: 8px;
}
.console-loading-dot {
width: 4px;
height: 4px;
width: 6px;
height: 6px;
background-color: #66d9ef;
border-radius: 50%;
background: var(--muted);
animation: pulse 1.2s ease-in-out infinite;
animation: bounce 1.3s infinite ease-in-out;
}
.console-loading-dot:nth-child(1) {
animation-delay: 0ms;
animation-delay: -0.32s;
}
.console-loading-dot:nth-child(2) {
animation-delay: 160ms;
animation-delay: -0.16s;
}
.console-loading-dot:nth-child(3) {
animation-delay: 320ms;
/* Logo */
.logo {
width: var(--logo-size);
height: var(--logo-size);
}

View File

@@ -18,22 +18,16 @@ type templateData struct {
WakeEventsPath string
}
//go:embed html/loading_page.min.html
//go:embed html/loading_page.html
var loadingPage []byte
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
//go:embed html/style.css
var cssBytes []byte
//go:embed html/loading.min.js
//go:embed html/loading-min.js
var jsBytes []byte
func setNoStoreHeaders(header http.Header) {
header.Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
header.Set("Pragma", "no-cache")
header.Set("Expires", "0")
}
func (w *Watcher) writeLoadingPage(rw http.ResponseWriter) error {
msg := w.cfg.ContainerName() + " is starting..."
@@ -46,7 +40,9 @@ func (w *Watcher) writeLoadingPage(rw http.ResponseWriter) error {
data.WakeEventsPath = idlewatcher.WakeEventsPath
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
setNoStoreHeaders(rw.Header())
rw.Header().Set("Cache-Control", "no-cache")
rw.Header().Add("Cache-Control", "no-store")
rw.Header().Add("Cache-Control", "must-revalidate")
rw.Header().Add("Connection", "close")
err := loadingPageTmpl.Execute(rw, data)
return err

View File

@@ -6,6 +6,7 @@ import (
"path/filepath"
"reflect"
"github.com/bytedance/sonic"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
@@ -51,6 +52,10 @@ func loadNS[T store](ns namespace) T {
store := reflect.New(reflect.TypeFor[T]().Elem()).Interface().(T)
store.Initialize()
if common.IsTest {
return store
}
path := filepath.Join(storesPath, string(ns)+".json")
file, err := os.Open(path)
if err != nil {
@@ -61,12 +66,13 @@ func loadNS[T store](ns namespace) T {
}
} else {
defer file.Close()
if err := json.NewDecoder(file).Decode(&store); err != nil {
if err := sonic.ConfigDefault.NewDecoder(file).Decode(&store); err != nil {
log.Err(err).
Str("path", path).
Msg("failed to load store")
}
}
stores[ns] = store
log.Debug().
Str("namespace", string(ns)).
Str("path", path).
@@ -78,7 +84,7 @@ func save() error {
errs := gperr.NewBuilder("failed to save data stores")
for ns, store := range stores {
path := filepath.Join(storesPath, string(ns)+".json")
if err := serialization.SaveFile(path, &store, 0o644, json.Marshal); err != nil {
if err := serialization.SaveFile(path, &store, 0o644, sonic.Marshal); err != nil {
errs.Add(err)
}
}
@@ -108,12 +114,12 @@ func (s *MapStore[VT]) Initialize() {
}
func (s MapStore[VT]) MarshalJSON() ([]byte, error) {
return json.Marshal(xsync.ToPlainMap(s.Map))
return sonic.Marshal(xsync.ToPlainMap(s.Map))
}
func (s *MapStore[VT]) UnmarshalJSON(data []byte) error {
tmp := make(map[string]VT)
if err := json.Unmarshal(data, &tmp); err != nil {
if err := sonic.Unmarshal(data, &tmp); err != nil {
return err
}
s.Map = xsync.NewMap[string, VT](xsync.WithPresize(len(tmp)))
@@ -129,10 +135,10 @@ func (obj *ObjectStore[Ptr]) Initialize() {
}
func (obj ObjectStore[Ptr]) MarshalJSON() ([]byte, error) {
return json.Marshal(obj.ptr)
return sonic.Marshal(obj.ptr)
}
func (obj *ObjectStore[Ptr]) UnmarshalJSON(data []byte) error {
obj.Initialize()
return json.Unmarshal(data, obj.ptr)
return sonic.Unmarshal(data, obj.ptr)
}

View File

@@ -4,18 +4,7 @@ import (
"testing"
)
func setupTest(t *testing.T) {
prevStoresPath := storesPath
storesPath = t.TempDir()
t.Cleanup(func() {
storesPath = prevStoresPath
clear(stores)
})
}
func TestNewJSON(t *testing.T) {
setupTest(t)
store := Store[string]("test")
store.Store("a", "1")
if v, _ := store.Load("a"); v != "1" {
@@ -24,8 +13,9 @@ func TestNewJSON(t *testing.T) {
}
func TestSaveLoadStore(t *testing.T) {
setupTest(t)
defer clear(stores)
storesPath = t.TempDir()
store := Store[string]("test")
store.Store("a", "1")
if err := save(); err != nil {
@@ -54,8 +44,9 @@ type testObject struct {
func (*testObject) Initialize() {}
func TestSaveLoadObject(t *testing.T) {
setupTest(t)
defer clear(stores)
storesPath = t.TempDir()
obj := Object[*testObject]("test")
obj.I = 1
obj.S = "1"

View File

@@ -0,0 +1,36 @@
package maxmind
import (
"github.com/puzpuzpuz/xsync/v4"
)
var cityCache = xsync.NewMap[string, *City]()
func (cfg *MaxMind) lookupCity(ip *IPInfo) (*City, bool) {
if ip.City != nil {
return ip.City, true
}
if cfg.db.Reader == nil {
return nil, false
}
city, ok := cityCache.Load(ip.Str)
if ok {
ip.City = city
return city, true
}
cfg.db.RLock()
defer cfg.db.RUnlock()
city = new(City)
err := cfg.db.Lookup(ip.IP, city)
if err != nil {
return nil, false
}
cityCache.Store(ip.Str, city)
ip.City = city
return city, true
}

View File

@@ -2,24 +2,16 @@ package maxmind
import (
"sync"
"sync/atomic"
"time"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/goutils/task"
"golang.org/x/time/rate"
)
var instance *MaxMind
var (
warnOnce sync.Once
errLogRateLimiter = rate.NewLimiter(rate.Every(3*time.Second), 1)
errLogSuppressedCounts = xsync.NewMap[string, *atomic.Int64](xsync.WithPresize(32))
)
var warnOnce sync.Once
func warnNotConfigured() {
log.Warn().Msg("MaxMind not configured, geo lookup will fail")
@@ -45,53 +37,9 @@ func HasInstance() bool {
}
func LookupCity(ip *IPInfo) (*City, bool) {
if ip.City != nil {
return ip.City, false
}
if instance == nil {
warnOnce.Do(warnNotConfigured)
return nil, false
}
city, err := lookupCityNoContext(ip.Str)
if err != nil {
logLookupCityError(ip.Str, err)
return nil, false
}
ip.City = city
return city, true
}
func lookupCityErrorKey(err error) string {
return err.Error()
}
func incrementSuppressedLookupCityError(err error) {
key := lookupCityErrorKey(err)
counter, _ := errLogSuppressedCounts.LoadOrCompute(key, func() (*atomic.Int64, bool) {
return &atomic.Int64{}, false
})
counter.Add(1)
}
func flushSuppressedLookupCityError(err error) int64 {
counter, ok := errLogSuppressedCounts.Load(lookupCityErrorKey(err))
if !ok {
return 0
}
return counter.Swap(0)
}
func logLookupCityError(ipStr string, err error) {
if !errLogRateLimiter.Allow() {
incrementSuppressedLookupCityError(err)
return
}
event := log.Err(err).Str("ip", ipStr)
if suppressedCount := flushSuppressedLookupCityError(err); suppressedCount > 0 {
event = event.Int64("suppressed_count", suppressedCount)
}
event.Msg("failed to lookup city")
return instance.lookupCity(ip)
}

View File

@@ -1,40 +0,0 @@
package maxmind
import (
"context"
"errors"
"net"
"github.com/yusing/goutils/cache"
)
var ErrInvalidIP = errors.New("invalid IP address")
var ErrDBNotLoaded = errors.New("maxmind database not loaded")
var lookupCityFn = cache.NewKeyFunc(func(_ context.Context, ipStr string) (*City, error) {
return instance.lookupCityReal(ipStr)
}).WithMaxEntries(1000).Build()
func lookupCityNoContext(ipStr string) (*City, error) {
return lookupCityFn(context.Background(), ipStr)
}
func (cfg *MaxMind) lookupCityReal(ipStr string) (*City, error) {
cfg.db.RLock()
defer cfg.db.RUnlock()
if cfg.db.Reader == nil {
return nil, ErrDBNotLoaded
}
city := new(City)
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, ErrInvalidIP
}
err := cfg.db.Lookup(ip, city)
if err != nil {
return nil, err
}
return city, nil
}

View File

@@ -1,87 +0,0 @@
package maxmind
import (
"bytes"
"context"
"encoding/json"
"errors"
"sync/atomic"
"testing"
"time"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/time/rate"
)
func TestLookupCityPropagatesContext(t *testing.T) {
oldLookupCityFn := lookupCityFn
t.Cleanup(func() {
lookupCityFn = oldLookupCityFn
})
type contextKey string
const wantIP = "1.1.1.1"
const wantValue contextKey = "trace-id"
lookupCityFn = func(ctx context.Context, ipStr string) (*City, error) {
require.Equal(t, wantIP, ipStr)
require.Equal(t, "trace-123", ctx.Value(wantValue))
return &City{}, nil
}
ctx := context.WithValue(t.Context(), wantValue, "trace-123")
city, err := lookupCityFn(ctx, wantIP)
require.NoError(t, err)
require.NotNil(t, city)
}
func TestLookupCityRealReturnsErrDBNotLoaded(t *testing.T) {
cfg := &MaxMind{}
city, err := cfg.lookupCityReal("1.1.1.1")
require.ErrorIs(t, err, ErrDBNotLoaded)
assert.Nil(t, city)
}
func TestLogLookupCityErrorIncludesSuppressedCount(t *testing.T) {
oldLimiter := errLogRateLimiter
oldSuppressed := errLogSuppressedCounts
oldLogger := log.Logger
t.Cleanup(func() {
errLogRateLimiter = oldLimiter
errLogSuppressedCounts = oldSuppressed
log.Logger = oldLogger
})
errLogRateLimiter = rate.NewLimiter(rate.Every(24*time.Hour), 1)
errLogSuppressedCounts = xsync.NewMap[string, *atomic.Int64]()
var buf bytes.Buffer
log.Logger = zerolog.New(&buf)
err := errors.New("boom")
logLookupCityError("1.1.1.1", err)
logLookupCityError("1.1.1.1", err)
errLogRateLimiter = rate.NewLimiter(rate.Inf, 1)
logLookupCityError("1.1.1.1", err)
lines := bytes.Split(bytes.TrimSpace(buf.Bytes()), []byte("\n"))
require.Len(t, lines, 2)
var entry map[string]any
require.NoError(t, json.Unmarshal(lines[1], &entry))
assert.Equal(t, "failed to lookup city", entry["message"])
assert.Equal(t, float64(1), entry["suppressed_count"])
buf.Reset()
logLookupCityError("1.1.1.1", err)
entry = map[string]any{}
require.NoError(t, json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &entry))
_, hasSuppressedCount := entry["suppressed_count"]
assert.False(t, hasSuppressedCount)
}

View File

@@ -106,10 +106,8 @@ func (cfg *MaxMind) LoadMaxMindDB(parent task.Parent) error {
} else {
cfg.Logger().Info().Msg("MaxMind DB loaded")
cfg.db.Reader = reader
if !common.IsTest {
go cfg.scheduleUpdate(parent)
}
}
return nil
}

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