mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-24 03:14:50 +01:00
Compare commits
32 Commits
feat/rules
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bb36e2e83 | ||
|
|
4b57ef1cad | ||
|
|
3850a4a6e7 | ||
|
|
da3c624582 | ||
|
|
157a83bef8 | ||
|
|
d61bd5ce51 | ||
|
|
bad3e9a989 | ||
|
|
9adfd73121 | ||
|
|
4a652aaf55 | ||
|
|
16c986978d | ||
|
|
107b7c5f64 | ||
|
|
818d75c8b7 | ||
|
|
f1bc5de3ea | ||
|
|
425ff0b25c | ||
|
|
1f6614e337 | ||
|
|
9ba102a33d | ||
|
|
31c616246b | ||
|
|
390859bd1f | ||
|
|
243662c13b | ||
|
|
588e9f5b18 | ||
|
|
a3bf88cc9c | ||
|
|
9b1af57859 | ||
|
|
bb7471cc9c | ||
|
|
a403b2b629 | ||
|
|
54b9e7f236 | ||
|
|
45b89cd452 | ||
|
|
72fea96c7b | ||
|
|
aef646be6f | ||
|
|
135a4ff6c7 | ||
|
|
5f418b62c7 | ||
|
|
bd92c46375 | ||
|
|
21a23dd147 |
@@ -56,10 +56,6 @@ GODOXY_HTTP3_ENABLED=true
|
|||||||
# API listening address
|
# API listening address
|
||||||
GODOXY_API_ADDR=127.0.0.1:8888
|
GODOXY_API_ADDR=127.0.0.1:8888
|
||||||
|
|
||||||
# Local API listening address (unauthenticated, optional)
|
|
||||||
# Useful for local development, debugging or automation
|
|
||||||
GODOXY_LOCAL_API_ADDR=
|
|
||||||
|
|
||||||
# Metrics
|
# Metrics
|
||||||
GODOXY_METRICS_DISABLE_CPU=false
|
GODOXY_METRICS_DISABLE_CPU=false
|
||||||
GODOXY_METRICS_DISABLE_MEMORY=false
|
GODOXY_METRICS_DISABLE_MEMORY=false
|
||||||
|
|||||||
60
.github/workflows/cli-binary.yml
vendored
60
.github/workflows/cli-binary.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: GoDoxy CLI Binary
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "cmd/cli/**"
|
|
||||||
- "internal/api/v1/docs/swagger.json"
|
|
||||||
- "Makefile"
|
|
||||||
- ".github/workflows/cli-binary.yml"
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "cmd/cli/**"
|
|
||||||
- "internal/api/v1/docs/swagger.json"
|
|
||||||
- "Makefile"
|
|
||||||
- ".github/workflows/cli-binary.yml"
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- runner: ubuntu-latest
|
|
||||||
platform: linux/amd64
|
|
||||||
binary_name: godoxy-cli-linux-amd64
|
|
||||||
- runner: ubuntu-24.04-arm
|
|
||||||
platform: linux/arm64
|
|
||||||
binary_name: godoxy-cli-linux-arm64
|
|
||||||
name: Build ${{ matrix.platform }}
|
|
||||||
runs-on: ${{ matrix.runner }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: "recursive"
|
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
|
|
||||||
- name: Verify dependencies
|
|
||||||
run: go mod verify
|
|
||||||
|
|
||||||
- name: Build CLI
|
|
||||||
run: |
|
|
||||||
make CLI_BIN_PATH=bin/${{ matrix.binary_name }} build-cli
|
|
||||||
|
|
||||||
- name: Check binary
|
|
||||||
run: |
|
|
||||||
file bin/${{ matrix.binary_name }}
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.binary_name }}
|
|
||||||
path: bin/${{ matrix.binary_name }}
|
|
||||||
19
.gitignore
vendored
19
.gitignore
vendored
@@ -14,33 +14,34 @@ data/
|
|||||||
debug/
|
debug/
|
||||||
|
|
||||||
logs/
|
logs/
|
||||||
|
log/
|
||||||
|
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
!cmd/**/
|
!cmd/**/
|
||||||
!internal/**/
|
!internal/**/
|
||||||
|
|
||||||
todo.md
|
todo.md
|
||||||
|
|
||||||
.*.swp
|
.*.swp
|
||||||
|
.aider*
|
||||||
mtrace.json
|
mtrace.json
|
||||||
.env
|
.env
|
||||||
*.env
|
*.env
|
||||||
|
.cursorrules
|
||||||
.cursor/
|
.cursor/
|
||||||
|
.windsurfrules
|
||||||
test.Dockerfile
|
test.Dockerfile
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
||||||
!agent.compose.yml
|
!agent.compose.yml
|
||||||
!agent/pkg/**
|
!agent/pkg/**
|
||||||
dev-data/
|
dev-data/
|
||||||
|
|
||||||
RELEASE_NOTES.md
|
RELEASE_NOTES.md
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
.kilocode/**
|
.kilocode/**
|
||||||
|
|
||||||
!.trunk/configs
|
|
||||||
|
|
||||||
# minified files
|
|
||||||
**/*-min.*
|
|
||||||
|
|
||||||
# generated CLI commands
|
|
||||||
cmd/cli/generated_commands.go
|
|
||||||
@@ -47,7 +47,6 @@ linters:
|
|||||||
errcheck:
|
errcheck:
|
||||||
exclude-functions:
|
exclude-functions:
|
||||||
- fmt.Fprintln
|
- fmt.Fprintln
|
||||||
- (*gin.Context).Error # gin context error handler
|
|
||||||
forbidigo:
|
forbidigo:
|
||||||
forbid:
|
forbid:
|
||||||
- pattern: ^print(ln)?$
|
- pattern: ^print(ln)?$
|
||||||
@@ -56,15 +55,21 @@ linters:
|
|||||||
statements: 120
|
statements: 120
|
||||||
gocyclo:
|
gocyclo:
|
||||||
min-complexity: 14
|
min-complexity: 14
|
||||||
godoclint:
|
|
||||||
ignore: internal/api/v1/.+
|
|
||||||
godox:
|
godox:
|
||||||
keywords:
|
keywords:
|
||||||
- FIXME
|
- FIXME
|
||||||
|
gomoddirectives:
|
||||||
|
replace-allow-list:
|
||||||
|
- github.com/abbot/go-http-auth
|
||||||
|
- github.com/gorilla/mux
|
||||||
|
- github.com/mailgun/minheap
|
||||||
|
- github.com/mailgun/multibuf
|
||||||
|
- github.com/jaguilar/vt100
|
||||||
|
- github.com/cucumber/godog
|
||||||
|
- github.com/http-wasm/http-wasm-host-go
|
||||||
govet:
|
govet:
|
||||||
disable:
|
disable:
|
||||||
- shadow
|
- shadow
|
||||||
- fieldalignment
|
|
||||||
enable-all: true
|
enable-all: true
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
locale: US
|
||||||
@@ -101,7 +106,8 @@ linters:
|
|||||||
checks:
|
checks:
|
||||||
- all
|
- all
|
||||||
- -SA1019
|
- -SA1019
|
||||||
- -QF1008 # keep embedded field selector for clarity
|
dot-import-whitelist:
|
||||||
|
- github.com/yusing/godoxy/internal/utils/testing
|
||||||
tagalign:
|
tagalign:
|
||||||
align: false
|
align: false
|
||||||
sort: true
|
sort: true
|
||||||
@@ -129,8 +135,9 @@ linters:
|
|||||||
- legacy
|
- legacy
|
||||||
- std-error-handling
|
- std-error-handling
|
||||||
paths:
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
- examples$
|
- examples$
|
||||||
- internal/api/v1/.+
|
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
- gofmt
|
- gofmt
|
||||||
@@ -139,7 +146,6 @@ formatters:
|
|||||||
exclusions:
|
exclusions:
|
||||||
generated: lax
|
generated: lax
|
||||||
paths:
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
- examples$
|
- examples$
|
||||||
- internal/api/v1/.+
|
|
||||||
run:
|
|
||||||
tests: false
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
# Prettier friendly markdownlint config (all formatting rules disabled)
|
|
||||||
extends: markdownlint/style/prettier
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
rules:
|
|
||||||
quoted-strings:
|
|
||||||
required: only-when-needed
|
|
||||||
extra-allowed: ["{|}"]
|
|
||||||
key-duplicates: {}
|
|
||||||
octal-values:
|
|
||||||
forbid-implicit-octal: true
|
|
||||||
@@ -7,45 +7,36 @@ cli:
|
|||||||
plugins:
|
plugins:
|
||||||
sources:
|
sources:
|
||||||
- id: trunk
|
- id: trunk
|
||||||
ref: v1.7.4
|
ref: v1.7.2
|
||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
||||||
runtimes:
|
runtimes:
|
||||||
enabled:
|
enabled:
|
||||||
- node@22.16.0
|
- node@22.16.0
|
||||||
- python@3.10.8
|
- python@3.10.8
|
||||||
- go@1.26.0
|
- go@1.24.3
|
||||||
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
||||||
lint:
|
lint:
|
||||||
disabled:
|
disabled:
|
||||||
- bandit
|
- markdownlint
|
||||||
- black
|
- yamllint
|
||||||
- isort
|
|
||||||
- ruff
|
|
||||||
enabled:
|
enabled:
|
||||||
- yamllint@1.38.0
|
- checkov@3.2.471
|
||||||
- markdownlint@0.47.0
|
- golangci-lint2@2.5.0
|
||||||
- checkov@3.2.501
|
|
||||||
- golangci-lint2@2.9.0
|
|
||||||
- hadolint@2.14.0
|
- hadolint@2.14.0
|
||||||
- actionlint@1.7.10
|
- actionlint@1.7.7
|
||||||
- git-diff-check
|
- git-diff-check
|
||||||
- gofmt@1.20.4
|
- gofmt@1.20.4
|
||||||
- osv-scanner@2.3.3
|
- osv-scanner@2.2.2
|
||||||
- oxipng@10.1.0
|
- oxipng@9.1.5
|
||||||
- prettier@3.8.1
|
- prettier@3.6.2
|
||||||
- shellcheck@0.11.0
|
- shellcheck@0.11.0
|
||||||
- shfmt@3.6.0
|
- shfmt@3.6.0
|
||||||
- trufflehog@3.93.3
|
- trufflehog@3.90.8
|
||||||
ignore:
|
|
||||||
- linters: [ALL]
|
|
||||||
paths:
|
|
||||||
- internal/api/v1/docs/**
|
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
disabled:
|
disabled:
|
||||||
- trunk-announce
|
- trunk-announce
|
||||||
enabled:
|
|
||||||
- trunk-upgrade-available
|
|
||||||
- trunk-check-pre-push
|
- trunk-check-pre-push
|
||||||
- trunk-fmt-pre-commit
|
- trunk-fmt-pre-commit
|
||||||
|
enabled:
|
||||||
|
- trunk-upgrade-available
|
||||||
|
|||||||
6
.vscode/settings.example.json
vendored
6
.vscode/settings.example.json
vendored
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"yaml.schemas": {
|
"yaml.schemas": {
|
||||||
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/config.schema.json": [
|
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/config.schema.json": [
|
||||||
"config.example.yml",
|
"config.example.yml",
|
||||||
"config.yml"
|
"config.yml"
|
||||||
],
|
],
|
||||||
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/routes.schema.json": [
|
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/routes.schema.json": [
|
||||||
"providers.example.yml"
|
"providers.example.yml"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
# Stage 1: deps
|
# Stage 1: deps
|
||||||
FROM golang:1.26.0-alpine AS deps
|
FROM golang:1.25.6-alpine AS deps
|
||||||
HEALTHCHECK NONE
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
# package version does not matter
|
# package version does not matter
|
||||||
# libgcc and libstdc++ are needed for bun
|
|
||||||
# trunk-ignore(hadolint/DL3018)
|
# trunk-ignore(hadolint/DL3018)
|
||||||
RUN apk add --no-cache tzdata make libcap-setcap libgcc libstdc++
|
RUN apk add --no-cache tzdata make libcap-setcap
|
||||||
|
|
||||||
ENV GOPATH=/root/go
|
ENV GOPATH=/root/go
|
||||||
ENV GOCACHE=/root/.cache/go-build
|
ENV GOCACHE=/root/.cache/go-build
|
||||||
@@ -18,10 +17,6 @@ COPY internal/gopsutil/go.mod internal/gopsutil/go.sum ./internal/gopsutil/
|
|||||||
COPY internal/go-proxmox/go.mod internal/go-proxmox/go.sum ./internal/go-proxmox/
|
COPY internal/go-proxmox/go.mod internal/go-proxmox/go.sum ./internal/go-proxmox/
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
# for minify-js
|
|
||||||
COPY --from=oven/bun:1.3.9-alpine /usr/local/bin/bun /usr/local/bin/bun
|
|
||||||
COPY --from=oven/bun:1.3.9-alpine /usr/local/bin/bunx /usr/local/bin/bunx
|
|
||||||
|
|
||||||
# remove godoxy stuff from go.mod first
|
# remove godoxy stuff from go.mod first
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
--mount=type=cache,target=/root/go/pkg/mod \
|
--mount=type=cache,target=/root/go/pkg/mod \
|
||||||
|
|||||||
59
Makefile
59
Makefile
@@ -1,5 +1,5 @@
|
|||||||
shell := /bin/sh
|
shell := /bin/sh
|
||||||
export VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null)
|
export VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||||
export BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
export BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
|
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
|
||||||
export GOOS = linux
|
export GOOS = linux
|
||||||
@@ -7,7 +7,7 @@ export GOOS = linux
|
|||||||
REPO_URL ?= https://github.com/yusing/godoxy
|
REPO_URL ?= https://github.com/yusing/godoxy
|
||||||
|
|
||||||
WEBUI_DIR ?= ../godoxy-webui
|
WEBUI_DIR ?= ../godoxy-webui
|
||||||
DOCS_DIR ?= wiki
|
DOCS_DIR ?= ${WEBUI_DIR}/wiki
|
||||||
|
|
||||||
ifneq ($(BRANCH), compat)
|
ifneq ($(BRANCH), compat)
|
||||||
GO_TAGS = sonic
|
GO_TAGS = sonic
|
||||||
@@ -58,7 +58,6 @@ endif
|
|||||||
|
|
||||||
BUILD_FLAGS += -tags '$(GO_TAGS)' -ldflags='$(LDFLAGS)'
|
BUILD_FLAGS += -tags '$(GO_TAGS)' -ldflags='$(LDFLAGS)'
|
||||||
BIN_PATH := $(shell pwd)/bin/${NAME}
|
BIN_PATH := $(shell pwd)/bin/${NAME}
|
||||||
CLI_BIN_PATH ?= $(shell pwd)/bin/godoxy-cli
|
|
||||||
|
|
||||||
export NAME
|
export NAME
|
||||||
export CGO_ENABLED
|
export CGO_ENABLED
|
||||||
@@ -93,7 +92,7 @@ docker-build-test:
|
|||||||
|
|
||||||
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
|
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
|
||||||
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
|
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
|
||||||
gomod_paths := $(shell find . -name go.mod -type f | grep -vE '^./internal/(go-oidc|go-proxmox|gopsutil)/' | xargs dirname)
|
gomod_paths := $(shell find . -name go.mod -type f | xargs dirname)
|
||||||
|
|
||||||
update-go:
|
update-go:
|
||||||
for file in ${files}; do \
|
for file in ${files}; do \
|
||||||
@@ -118,27 +117,12 @@ mod-tidy:
|
|||||||
cd ${PWD}/$$path && go mod tidy; \
|
cd ${PWD}/$$path && go mod tidy; \
|
||||||
done
|
done
|
||||||
|
|
||||||
minify-js:
|
build:
|
||||||
@if [ "${agent}" = "1" ]; then \
|
|
||||||
echo "minify-js: skipped for agent"; \
|
|
||||||
elif [ "${socket-proxy}" = "1" ]; then \
|
|
||||||
echo "minify-js: skipped for socket-proxy"; \
|
|
||||||
else \
|
|
||||||
for file in $$(find internal/ -name '*.js' | grep -v -- '-min\.js$$'); do \
|
|
||||||
ext="$${file##*.}"; \
|
|
||||||
base="$${file%.*}"; \
|
|
||||||
min_file="$${base}-min.$$ext"; \
|
|
||||||
echo "minifying $$file -> $$min_file"; \
|
|
||||||
bunx --bun uglify-js $$file --compress --mangle --output $$min_file; \
|
|
||||||
done \
|
|
||||||
fi
|
|
||||||
|
|
||||||
build: minify-js
|
|
||||||
mkdir -p $(shell dirname ${BIN_PATH})
|
mkdir -p $(shell dirname ${BIN_PATH})
|
||||||
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
|
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
|
||||||
${POST_BUILD}
|
${POST_BUILD}
|
||||||
|
|
||||||
run: minify-js
|
run:
|
||||||
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
|
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
@@ -148,12 +132,16 @@ dev-build: build
|
|||||||
docker compose -f dev.compose.yml up -t 0 -d app --force-recreate
|
docker compose -f dev.compose.yml up -t 0 -d app --force-recreate
|
||||||
|
|
||||||
benchmark:
|
benchmark:
|
||||||
@TARGETS="$(TARGET)"; \
|
@if [ -z "$(TARGET)" ]; then \
|
||||||
if [ -z "$$TARGETS" ]; then TARGETS="godoxy traefik caddy nginx"; fi; \
|
docker compose -f dev.compose.yml up -d --force-recreate godoxy traefik caddy nginx; \
|
||||||
trap 'docker compose -f dev.compose.yml down $$TARGETS' EXIT; \
|
else \
|
||||||
docker compose -f dev.compose.yml up -d --force-recreate $$TARGETS; \
|
docker compose -f dev.compose.yml up -d --force-recreate $(TARGET); \
|
||||||
sleep 1; \
|
fi
|
||||||
./scripts/benchmark.sh
|
sleep 1
|
||||||
|
@./scripts/benchmark.sh
|
||||||
|
|
||||||
|
dev-run: build
|
||||||
|
cd dev-data && ${BIN_PATH}
|
||||||
|
|
||||||
rapid-crash:
|
rapid-crash:
|
||||||
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
||||||
@@ -179,20 +167,17 @@ gen-swagger:
|
|||||||
python3 scripts/fix-swagger-json.py
|
python3 scripts/fix-swagger-json.py
|
||||||
# we don't need this
|
# we don't need this
|
||||||
rm internal/api/v1/docs/docs.go
|
rm internal/api/v1/docs/docs.go
|
||||||
cp internal/api/v1/docs/swagger.json ${DOCS_DIR}/public/api.json
|
|
||||||
|
gen-swagger-markdown: gen-swagger
|
||||||
|
# brew tap go-swagger/go-swagger && brew install go-swagger
|
||||||
|
swagger generate markdown -f internal/api/v1/docs/swagger.yaml --skip-validation --output ${DOCS_DIR}/src/API.md
|
||||||
|
|
||||||
gen-api-types: gen-swagger
|
gen-api-types: gen-swagger
|
||||||
# --disable-throw-on-error
|
# --disable-throw-on-error
|
||||||
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
||||||
--responses -o ${WEBUI_DIR}/src/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
||||||
|
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
|
||||||
|
|
||||||
gen-cli:
|
.PHONY: update-wiki
|
||||||
cd cmd/cli && go run ./gen
|
|
||||||
|
|
||||||
build-cli: gen-cli
|
|
||||||
mkdir -p $(shell dirname ${CLI_BIN_PATH})
|
|
||||||
go build -C cmd/cli -o ${CLI_BIN_PATH} .
|
|
||||||
|
|
||||||
.PHONY: gen-cli build-cli update-wiki
|
|
||||||
update-wiki:
|
update-wiki:
|
||||||
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts
|
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -36,6 +36,7 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
|||||||
- [Proxmox Integration](#proxmox-integration)
|
- [Proxmox Integration](#proxmox-integration)
|
||||||
- [Automatic Route Binding](#automatic-route-binding)
|
- [Automatic Route Binding](#automatic-route-binding)
|
||||||
- [WebUI Management](#webui-management)
|
- [WebUI Management](#webui-management)
|
||||||
|
- [API Endpoints](#api-endpoints)
|
||||||
- [Update / Uninstall system agent](#update--uninstall-system-agent)
|
- [Update / Uninstall system agent](#update--uninstall-system-agent)
|
||||||
- [Screenshots](#screenshots)
|
- [Screenshots](#screenshots)
|
||||||
- [idlesleeper](#idlesleeper)
|
- [idlesleeper](#idlesleeper)
|
||||||
@@ -166,8 +167,23 @@ routes:
|
|||||||
From the WebUI, you can:
|
From the WebUI, you can:
|
||||||
|
|
||||||
- **LXC Lifecycle Control**: Start, stop, restart containers
|
- **LXC Lifecycle Control**: Start, stop, restart containers
|
||||||
- **Node Logs**: Stream real-time journalctl or log files output from nodes
|
- **Node Logs**: Stream real-time journalctl output from nodes
|
||||||
- **LXC Logs**: Stream real-time journalctl or log files output from containers
|
- **LXC Logs**: Stream real-time journalctl output from containers
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
```http
|
||||||
|
# Node journalctl (WebSocket)
|
||||||
|
GET /api/v1/proxmox/journalctl/:node
|
||||||
|
|
||||||
|
# LXC journalctl (WebSocket)
|
||||||
|
GET /api/v1/proxmox/journalctl/:node/:vmid
|
||||||
|
|
||||||
|
# LXC lifecycle control
|
||||||
|
POST /api/v1/proxmox/lxc/:node/:vmid/start
|
||||||
|
POST /api/v1/proxmox/lxc/:node/:vmid/stop
|
||||||
|
POST /api/v1/proxmox/lxc/:node/:vmid/restart
|
||||||
|
```
|
||||||
|
|
||||||
## Update / Uninstall system agent
|
## Update / Uninstall system agent
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
- [Proxmox 整合](#proxmox-整合)
|
- [Proxmox 整合](#proxmox-整合)
|
||||||
- [自動路由綁定](#自動路由綁定)
|
- [自動路由綁定](#自動路由綁定)
|
||||||
- [WebUI 管理](#webui-管理)
|
- [WebUI 管理](#webui-管理)
|
||||||
|
- [API 端點](#api-端點)
|
||||||
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
|
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
|
||||||
- [截圖](#截圖)
|
- [截圖](#截圖)
|
||||||
- [閒置休眠](#閒置休眠)
|
- [閒置休眠](#閒置休眠)
|
||||||
@@ -182,8 +183,23 @@ routes:
|
|||||||
您可以從 WebUI:
|
您可以從 WebUI:
|
||||||
|
|
||||||
- **LXC 生命週期控制**:啟動、停止、重新啟動容器
|
- **LXC 生命週期控制**:啟動、停止、重新啟動容器
|
||||||
- **節點日誌**:串流節點的即時 journalctl 或日誌檔案輸出
|
- **節點日誌**:串流來自節點的即時 journalctl 輸出
|
||||||
- **LXC 日誌**:串流容器的即時 journalctl 或日誌檔案輸出
|
- **LXC 日誌**:串流來自容器的即時 journalctl 輸出
|
||||||
|
|
||||||
|
### API 端點
|
||||||
|
|
||||||
|
```http
|
||||||
|
# 節點 journalctl (WebSocket)
|
||||||
|
GET /api/v1/proxmox/journalctl/:node
|
||||||
|
|
||||||
|
# LXC journalctl (WebSocket)
|
||||||
|
GET /api/v1/proxmox/journalctl/:node/:vmid
|
||||||
|
|
||||||
|
# LXC 生命週期控制
|
||||||
|
POST /api/v1/proxmox/lxc/:node/:vmid/start
|
||||||
|
POST /api/v1/proxmox/lxc/:node/:vmid/stop
|
||||||
|
POST /api/v1/proxmox/lxc/:node/:vmid/restart
|
||||||
|
```
|
||||||
|
|
||||||
## 更新 / 卸載系統代理 (System Agent)
|
## 更新 / 卸載系統代理 (System Agent)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/yusing/godoxy/agent/pkg/handler"
|
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||||
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
"github.com/yusing/goutils/version"
|
"github.com/yusing/goutils/version"
|
||||||
@@ -71,7 +72,7 @@ Tips:
|
|||||||
// - Otherwise: route to HTTPS API handler
|
// - Otherwise: route to HTTPS API handler
|
||||||
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: env.AgentPort})
|
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: env.AgentPort})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to listen on port")
|
gperr.LogFatal("failed to listen on port", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
caCertPool := x509.NewCertPool()
|
caCertPool := x509.NewCertPool()
|
||||||
@@ -147,7 +148,7 @@ Tips:
|
|||||||
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
|
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
|
||||||
l, err := net.Listen("tcp", socketproxy.ListenAddr)
|
l, err := net.Listen("tcp", socketproxy.ListenAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to listen on port")
|
gperr.LogFatal("failed to listen on port", err)
|
||||||
}
|
}
|
||||||
errLog := log.Logger.With().Str("level", "error").Str("component", "socketproxy").Logger()
|
errLog := log.Logger.With().Str("level", "error").Str("component", "socketproxy").Logger()
|
||||||
srv := http.Server{
|
srv := http.Server{
|
||||||
@@ -157,15 +158,10 @@ Tips:
|
|||||||
},
|
},
|
||||||
ErrorLog: stdlog.New(&errLog, "", 0),
|
ErrorLog: stdlog.New(&errLog, "", 0),
|
||||||
}
|
}
|
||||||
go func() {
|
srv.Serve(l)
|
||||||
err := srv.Serve(l)
|
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
log.Error().Err(err).Msg("socket proxy server stopped with error")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
systeminfo.Poller.Start(t)
|
systeminfo.Poller.Start()
|
||||||
|
|
||||||
task.WaitExit(3)
|
task.WaitExit(3)
|
||||||
}
|
}
|
||||||
|
|||||||
40
agent/go.mod
40
agent/go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/yusing/godoxy/agent
|
module github.com/yusing/godoxy/agent
|
||||||
|
|
||||||
go 1.26.0
|
go 1.25.6
|
||||||
|
|
||||||
exclude (
|
exclude (
|
||||||
github.com/moby/moby/api v1.53.0 // allow older daemon versions
|
github.com/moby/moby/api v1.53.0 // allow older daemon versions
|
||||||
@@ -23,11 +23,11 @@ require (
|
|||||||
github.com/bytedance/sonic v1.15.0
|
github.com/bytedance/sonic v1.15.0
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/pion/dtls/v3 v3.1.2
|
github.com/pion/dtls/v3 v3.0.10
|
||||||
github.com/pion/transport/v3 v3.1.1
|
github.com/pion/transport/v3 v3.1.1
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/yusing/godoxy v0.26.0
|
github.com/yusing/godoxy v0.25.2
|
||||||
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||||
github.com/yusing/goutils v0.7.0
|
github.com/yusing/goutils v0.7.0
|
||||||
)
|
)
|
||||||
@@ -43,12 +43,12 @@ require (
|
|||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/cli v29.2.1+incompatible // indirect
|
github.com/docker/cli v29.2.0+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/ebitengine/purego v0.9.1 // indirect
|
github.com/ebitengine/purego v0.9.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
@@ -60,10 +60,10 @@ require (
|
|||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.3 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
@@ -81,7 +81,7 @@ require (
|
|||||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
|
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/shirou/gopsutil/v4 v4.26.1 // indirect
|
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
@@ -90,20 +90,20 @@ require (
|
|||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||||
github.com/yusing/ds v0.4.1 // indirect
|
github.com/yusing/ds v0.4.1 // indirect
|
||||||
github.com/yusing/gointernals v0.2.0 // indirect
|
github.com/yusing/gointernals v0.1.16 // indirect
|
||||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260218062549-0b0fa3a059ec // indirect
|
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468 // indirect
|
||||||
github.com/yusing/goutils/http/websocket v0.0.0-20260218062549-0b0fa3a059ec // indirect
|
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
golang.org/x/arch v0.24.0 // indirect
|
golang.org/x/arch v0.23.0 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.47.0 // indirect
|
||||||
golang.org/x/net v0.50.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
104
agent/go.sum
104
agent/go.sum
@@ -37,8 +37,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
|
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
||||||
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@@ -49,14 +49,14 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
|||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
|
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||||
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
|
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
@@ -93,14 +93,14 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
|||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
|
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||||
github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
|
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
|
||||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -111,10 +111,10 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/luthermonson/go-proxmox v0.4.0 h1:LKXpG9d64zTaQF79wV0kfOnnSwIcdG39m7sc4ga+XZs=
|
github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=
|
||||||
github.com/luthermonson/go-proxmox v0.4.0/go.mod h1:U6dAkJ+iiwaeb1g/LMWpWuWN4nmvWeXhmoMuYJMumS4=
|
github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
@@ -145,16 +145,16 @@ github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5
|
|||||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
|
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||||
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
|
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
|
||||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
@@ -174,10 +174,10 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
|||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
|
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||||
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
|
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||||
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
|
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
|
||||||
github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k=
|
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
@@ -210,38 +210,38 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
|||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
||||||
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||||
github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtSVvg=
|
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||||
github.com/yusing/gointernals v0.2.0/go.mod h1:xGzNbPGMm5Z8kG0t4JYISMscw+gMQlgghkLxlgRZv5Y=
|
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -250,14 +250,14 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# agent/pkg/agent
|
# Agent Package
|
||||||
|
|
||||||
The `agent` package provides the client-side implementation for interacting with GoDoxy agents. It handles agent configuration, secure communication via TLS, and provides utilities for agent deployment and management.
|
The `agent` package provides the client-side implementation for interacting with GoDoxy agents. It handles agent configuration, secure communication via TLS, and provides utilities for agent deployment and management.
|
||||||
|
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
|
|||||||
cfg.l = log.With().Str("agent", cfg.Name).Logger()
|
cfg.l = log.With().Str("agent", cfg.Name).Logger()
|
||||||
|
|
||||||
if err := streamUnsupportedErrs.Error(); err != nil {
|
if err := streamUnsupportedErrs.Error(); err != nil {
|
||||||
cfg.l.Warn().Err(err).Msg("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work")
|
gperr.LogWarn("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work", err, &cfg.l)
|
||||||
}
|
}
|
||||||
|
|
||||||
if serverVersion.IsNewerThanMajor(cfg.Version) {
|
if serverVersion.IsNewerThanMajor(cfg.Version) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# agent/pkg/agent/stream
|
# Stream proxy protocol
|
||||||
|
|
||||||
This package implements a small header-based handshake that allows an authenticated client to request forwarding to a `(host, port)` destination. It supports both TCP-over-TLS and UDP-over-DTLS transports.
|
This package implements a small header-based handshake that allows an authenticated client to request forwarding to a `(host, port)` destination. It supports both TCP-over-TLS and UDP-over-DTLS transports.
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,8 @@ const (
|
|||||||
|
|
||||||
var version = [versionSize]byte{'0', '.', '1', '.', '0', 0, 0, 0}
|
var version = [versionSize]byte{'0', '.', '1', '.', '0', 0, 0, 0}
|
||||||
|
|
||||||
var (
|
var ErrInvalidHeader = errors.New("invalid header")
|
||||||
ErrInvalidHeader = errors.New("invalid header")
|
var ErrCloseImmediately = errors.New("close immediately")
|
||||||
ErrCloseImmediately = errors.New("close immediately")
|
|
||||||
)
|
|
||||||
|
|
||||||
type FlagType uint8
|
type FlagType uint8
|
||||||
|
|
||||||
|
|||||||
@@ -45,10 +45,9 @@ func TestTLSALPNMux_HTTPAndStreamShareOnePort(t *testing.T) {
|
|||||||
defer func() { _ = tlsLn.Close() }()
|
defer func() { _ = tlsLn.Close() }()
|
||||||
|
|
||||||
// HTTP server
|
// HTTP server
|
||||||
httpSrv := &http.Server{
|
httpSrv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
_, _ = w.Write([]byte("ok"))
|
||||||
_, _ = w.Write([]byte("ok"))
|
}),
|
||||||
}),
|
|
||||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
|
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
|
||||||
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
||||||
streamSrv.ServeConn(conn)
|
streamSrv.ServeConn(conn)
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ func TestUDPServer_RejectInvalidClient(t *testing.T) {
|
|||||||
|
|
||||||
srv := startUDPServer(t, certs)
|
srv := startUDPServer(t, certs)
|
||||||
|
|
||||||
|
|
||||||
// Try to connect with a client cert from a different CA
|
// Try to connect with a client cert from a different CA
|
||||||
_, err = stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, invalidClientCert)
|
_, err = stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, invalidClientCert)
|
||||||
require.Error(t, err, "expected error when connecting with client cert from different CA")
|
require.Error(t, err, "expected error when connecting with client cert from different CA")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.26.0-alpine AS builder
|
FROM golang:1.25.6-alpine AS builder
|
||||||
|
|
||||||
HEALTHCHECK NONE
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module github.com/yusing/godoxy/cmd/bench_server
|
module github.com/yusing/godoxy/cmd/bench_server
|
||||||
|
|
||||||
go 1.26.0
|
go 1.25.6
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"math/rand/v2"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"math/rand/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var printables = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
printables = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
var random = make([]byte, 4096)
|
||||||
random = make([]byte, 4096)
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
for i := range random {
|
for i := range random {
|
||||||
|
|||||||
730
cmd/cli/cli.go
730
cmd/cli/cli.go
@@ -1,730 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/yusing/goutils/env"
|
|
||||||
)
|
|
||||||
|
|
||||||
type config struct {
|
|
||||||
Addr string
|
|
||||||
}
|
|
||||||
|
|
||||||
type stringSliceFlag struct {
|
|
||||||
set bool
|
|
||||||
v []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stringSliceFlag) String() string {
|
|
||||||
return strings.Join(s.v, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stringSliceFlag) Set(value string) error {
|
|
||||||
s.set = true
|
|
||||||
if value == "" {
|
|
||||||
s.v = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s.v = strings.Split(value, ",")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(args []string) error {
|
|
||||||
cfg, rest, err := parseGlobal(args)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(rest) == 0 {
|
|
||||||
printHelp()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if rest[0] == "help" {
|
|
||||||
printHelp()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ep, matchedLen := findEndpoint(rest)
|
|
||||||
if ep == nil {
|
|
||||||
ep, matchedLen = findEndpointAlias(rest)
|
|
||||||
}
|
|
||||||
if ep == nil {
|
|
||||||
return unknownCommandError(rest)
|
|
||||||
}
|
|
||||||
cmdArgs := rest[matchedLen:]
|
|
||||||
return executeEndpoint(cfg.Addr, *ep, cmdArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseGlobal(args []string) (config, []string, error) {
|
|
||||||
var cfg config
|
|
||||||
fs := flag.NewFlagSet("godoxy", flag.ContinueOnError)
|
|
||||||
fs.SetOutput(io.Discard)
|
|
||||||
fs.StringVar(&cfg.Addr, "addr", "", "API address, e.g. 127.0.0.1:8888 or http://127.0.0.1:8888")
|
|
||||||
if err := fs.Parse(args); err != nil {
|
|
||||||
return cfg, nil, err
|
|
||||||
}
|
|
||||||
return cfg, fs.Args(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveBaseURL(addrFlag string) (string, error) {
|
|
||||||
if addrFlag != "" {
|
|
||||||
return normalizeURL(addrFlag), nil
|
|
||||||
}
|
|
||||||
_, _, _, fullURL := env.GetAddrEnv("LOCAL_API_ADDR", "", "http")
|
|
||||||
if fullURL == "" {
|
|
||||||
return "", errors.New("missing LOCAL_API_ADDR (or GODOXY_LOCAL_API_ADDR). set env var or pass --addr")
|
|
||||||
}
|
|
||||||
return normalizeURL(fullURL), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeURL(addr string) string {
|
|
||||||
a := strings.TrimSpace(addr)
|
|
||||||
if strings.Contains(a, "://") {
|
|
||||||
return strings.TrimRight(a, "/")
|
|
||||||
}
|
|
||||||
return "http://" + strings.TrimRight(a, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func findEndpoint(args []string) (*Endpoint, int) {
|
|
||||||
var best *Endpoint
|
|
||||||
bestLen := -1
|
|
||||||
for i := range generatedEndpoints {
|
|
||||||
ep := &generatedEndpoints[i]
|
|
||||||
if len(ep.CommandPath) > len(args) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ok := true
|
|
||||||
for j, tok := range ep.CommandPath {
|
|
||||||
if args[j] != tok {
|
|
||||||
ok = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ok && len(ep.CommandPath) > bestLen {
|
|
||||||
best = ep
|
|
||||||
bestLen = len(ep.CommandPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best, bestLen
|
|
||||||
}
|
|
||||||
|
|
||||||
func executeEndpoint(addrFlag string, ep Endpoint, args []string) error {
|
|
||||||
fs := flag.NewFlagSet(strings.Join(ep.CommandPath, "-"), flag.ContinueOnError)
|
|
||||||
fs.SetOutput(io.Discard)
|
|
||||||
useWS := false
|
|
||||||
if ep.IsWebSocket {
|
|
||||||
fs.BoolVar(&useWS, "ws", false, "use websocket")
|
|
||||||
}
|
|
||||||
typedValues := make(map[string]any, len(ep.Params))
|
|
||||||
isSet := make(map[string]bool, len(ep.Params))
|
|
||||||
for _, p := range ep.Params {
|
|
||||||
switch p.Type {
|
|
||||||
case "integer":
|
|
||||||
v := new(int)
|
|
||||||
fs.IntVar(v, p.FlagName, 0, p.Description)
|
|
||||||
typedValues[p.FlagName] = v
|
|
||||||
case "number":
|
|
||||||
v := new(float64)
|
|
||||||
fs.Float64Var(v, p.FlagName, 0, p.Description)
|
|
||||||
typedValues[p.FlagName] = v
|
|
||||||
case "boolean":
|
|
||||||
v := new(bool)
|
|
||||||
fs.BoolVar(v, p.FlagName, false, p.Description)
|
|
||||||
typedValues[p.FlagName] = v
|
|
||||||
case "array":
|
|
||||||
v := &stringSliceFlag{}
|
|
||||||
fs.Var(v, p.FlagName, p.Description+" (comma-separated)")
|
|
||||||
typedValues[p.FlagName] = v
|
|
||||||
default:
|
|
||||||
v := new(string)
|
|
||||||
fs.StringVar(v, p.FlagName, "", p.Description)
|
|
||||||
typedValues[p.FlagName] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := fs.Parse(args); err != nil {
|
|
||||||
return fmt.Errorf("%w\n\n%s", err, formatEndpointHelp(ep))
|
|
||||||
}
|
|
||||||
if len(fs.Args()) > 0 {
|
|
||||||
return fmt.Errorf("unexpected args: %s\n\n%s", strings.Join(fs.Args(), " "), formatEndpointHelp(ep))
|
|
||||||
}
|
|
||||||
fs.Visit(func(f *flag.Flag) {
|
|
||||||
isSet[f.Name] = true
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, p := range ep.Params {
|
|
||||||
if !p.Required {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !isSet[p.FlagName] {
|
|
||||||
return fmt.Errorf("missing required flag --%s\n\n%s", p.FlagName, formatEndpointHelp(ep))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
baseURL, err := resolveBaseURL(addrFlag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
reqURL, body, err := buildRequest(ep, baseURL, typedValues, isSet)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if useWS {
|
|
||||||
if !ep.IsWebSocket {
|
|
||||||
return errors.New("--ws is only supported for websocket endpoints")
|
|
||||||
}
|
|
||||||
return execWebsocket(ep, reqURL)
|
|
||||||
}
|
|
||||||
return execHTTP(ep, reqURL, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildRequest(ep Endpoint, baseURL string, typedValues map[string]any, isSet map[string]bool) (string, []byte, error) {
|
|
||||||
path := ep.Path
|
|
||||||
for _, p := range ep.Params {
|
|
||||||
if p.In != "path" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
raw, err := paramValueString(p, typedValues[p.FlagName], isSet[p.FlagName])
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
if raw == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
esc := url.PathEscape(raw)
|
|
||||||
path = strings.ReplaceAll(path, "{"+p.Name+"}", esc)
|
|
||||||
path = strings.ReplaceAll(path, ":"+p.Name, esc)
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(baseURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("invalid base url: %w", err)
|
|
||||||
}
|
|
||||||
u.Path = strings.TrimRight(u.Path, "/") + path
|
|
||||||
|
|
||||||
q := u.Query()
|
|
||||||
for _, p := range ep.Params {
|
|
||||||
if p.In != "query" || !isSet[p.FlagName] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val, err := paramQueryValues(p, typedValues[p.FlagName])
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
for _, v := range val {
|
|
||||||
q.Add(p.Name, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
bodyMap := map[string]any{}
|
|
||||||
rawBody := ""
|
|
||||||
for _, p := range ep.Params {
|
|
||||||
if p.In != "body" || !isSet[p.FlagName] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if p.Name == "file" {
|
|
||||||
s, err := paramValueString(p, typedValues[p.FlagName], true)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
rawBody = s
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
v, err := paramBodyValue(p, typedValues[p.FlagName])
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
bodyMap[p.Name] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if rawBody != "" {
|
|
||||||
return u.String(), []byte(rawBody), nil
|
|
||||||
}
|
|
||||||
if len(bodyMap) == 0 {
|
|
||||||
return u.String(), nil, nil
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(bodyMap)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("marshal body: %w", err)
|
|
||||||
}
|
|
||||||
return u.String(), data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func paramValueString(p Param, raw any, wasSet bool) (string, error) {
|
|
||||||
if !wasSet {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
switch v := raw.(type) {
|
|
||||||
case *string:
|
|
||||||
return *v, nil
|
|
||||||
case *int:
|
|
||||||
return strconv.Itoa(*v), nil
|
|
||||||
case *float64:
|
|
||||||
return strconv.FormatFloat(*v, 'f', -1, 64), nil
|
|
||||||
case *bool:
|
|
||||||
if *v {
|
|
||||||
return "true", nil
|
|
||||||
}
|
|
||||||
return "false", nil
|
|
||||||
case *stringSliceFlag:
|
|
||||||
return strings.Join(v.v, ","), nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("unsupported flag value for %s", p.FlagName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func paramQueryValues(p Param, raw any) ([]string, error) {
|
|
||||||
switch v := raw.(type) {
|
|
||||||
case *string:
|
|
||||||
return []string{*v}, nil
|
|
||||||
case *int:
|
|
||||||
return []string{strconv.Itoa(*v)}, nil
|
|
||||||
case *float64:
|
|
||||||
return []string{strconv.FormatFloat(*v, 'f', -1, 64)}, nil
|
|
||||||
case *bool:
|
|
||||||
if *v {
|
|
||||||
return []string{"true"}, nil
|
|
||||||
}
|
|
||||||
return []string{"false"}, nil
|
|
||||||
case *stringSliceFlag:
|
|
||||||
if len(v.v) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return v.v, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported query flag type for %s", p.FlagName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func paramBodyValue(p Param, raw any) (any, error) {
|
|
||||||
switch v := raw.(type) {
|
|
||||||
case *string:
|
|
||||||
if p.Type == "object" || p.Type == "array" {
|
|
||||||
var decoded any
|
|
||||||
if err := json.Unmarshal([]byte(*v), &decoded); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid JSON for --%s: %w", p.FlagName, err)
|
|
||||||
}
|
|
||||||
return decoded, nil
|
|
||||||
}
|
|
||||||
return *v, nil
|
|
||||||
case *int:
|
|
||||||
return *v, nil
|
|
||||||
case *float64:
|
|
||||||
return *v, nil
|
|
||||||
case *bool:
|
|
||||||
return *v, nil
|
|
||||||
case *stringSliceFlag:
|
|
||||||
return v.v, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported body flag type for %s", p.FlagName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func execHTTP(ep Endpoint, reqURL string, body []byte) error {
|
|
||||||
var r io.Reader
|
|
||||||
if body != nil {
|
|
||||||
r = bytes.NewReader(body)
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(ep.Method, reqURL, r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if body != nil {
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
}
|
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
payload, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
||||||
if len(payload) == 0 {
|
|
||||||
return fmt.Errorf("%s %s failed: %s", ep.Method, ep.Path, resp.Status)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%s %s failed: %s: %s", ep.Method, ep.Path, resp.Status, strings.TrimSpace(string(payload)))
|
|
||||||
}
|
|
||||||
|
|
||||||
printJSON(payload)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func execWebsocket(ep Endpoint, reqURL string) error {
|
|
||||||
wsURL := strings.Replace(reqURL, "http://", "ws://", 1)
|
|
||||||
wsURL = strings.Replace(wsURL, "https://", "wss://", 1)
|
|
||||||
if strings.ToUpper(ep.Method) != http.MethodGet {
|
|
||||||
return fmt.Errorf("--ws requires GET endpoint, got %s", ep.Method)
|
|
||||||
}
|
|
||||||
c, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
stopPing := make(chan struct{})
|
|
||||||
defer close(stopPing)
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(3 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stopPing:
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
_ = c.SetWriteDeadline(time.Now().Add(2 * time.Second))
|
|
||||||
if err := c.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
_, msg, err := c.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) || strings.Contains(err.Error(), "close") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if string(msg) == "pong" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Println(string(msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printJSON(payload []byte) {
|
|
||||||
if len(payload) == 0 {
|
|
||||||
fmt.Println("null")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var v any
|
|
||||||
if err := json.Unmarshal(payload, &v); err != nil {
|
|
||||||
fmt.Println(strings.TrimSpace(string(payload)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
_ = enc.Encode(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func printHelp() {
|
|
||||||
fmt.Println("godoxy [--addr ADDR] <command>")
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("Examples:")
|
|
||||||
fmt.Println(" godoxy version")
|
|
||||||
fmt.Println(" godoxy route list")
|
|
||||||
fmt.Println(" godoxy route route --which whoami")
|
|
||||||
fmt.Println()
|
|
||||||
printGroupedCommands()
|
|
||||||
}
|
|
||||||
|
|
||||||
func printGroupedCommands() {
|
|
||||||
grouped := map[string][]Endpoint{}
|
|
||||||
groupOrder := make([]string, 0)
|
|
||||||
seen := map[string]bool{}
|
|
||||||
for _, ep := range generatedEndpoints {
|
|
||||||
group := "root"
|
|
||||||
if len(ep.CommandPath) > 1 {
|
|
||||||
group = ep.CommandPath[0]
|
|
||||||
}
|
|
||||||
grouped[group] = append(grouped[group], ep)
|
|
||||||
if !seen[group] {
|
|
||||||
seen[group] = true
|
|
||||||
groupOrder = append(groupOrder, group)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(groupOrder)
|
|
||||||
for _, group := range groupOrder {
|
|
||||||
fmt.Printf("Commands (%s):\n", group)
|
|
||||||
sort.Slice(grouped[group], func(i, j int) bool {
|
|
||||||
li := strings.Join(grouped[group][i].CommandPath, " ")
|
|
||||||
lj := strings.Join(grouped[group][j].CommandPath, " ")
|
|
||||||
return li < lj
|
|
||||||
})
|
|
||||||
maxCmdWidth := 0
|
|
||||||
for _, ep := range grouped[group] {
|
|
||||||
cmd := strings.Join(ep.CommandPath, " ")
|
|
||||||
if len(cmd) > maxCmdWidth {
|
|
||||||
maxCmdWidth = len(cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, ep := range grouped[group] {
|
|
||||||
cmd := strings.Join(ep.CommandPath, " ")
|
|
||||||
fmt.Printf(" %-*s %s\n", maxCmdWidth, cmd, ep.Summary)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func unknownCommandError(rest []string) error {
|
|
||||||
cmd := strings.Join(rest, " ")
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString("unknown command: ")
|
|
||||||
b.WriteString(cmd)
|
|
||||||
if len(rest) > 0 && hasGroup(rest[0]) {
|
|
||||||
if len(rest) > 1 {
|
|
||||||
if hint := nearestForGroup(rest[0], rest[1]); hint != "" {
|
|
||||||
b.WriteString("\nDo you mean ")
|
|
||||||
b.WriteString(hint)
|
|
||||||
b.WriteString("?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
b.WriteString(formatGroupHelp(rest[0]))
|
|
||||||
return errors.New(b.String())
|
|
||||||
}
|
|
||||||
if hint := nearestCommand(cmd); hint != "" {
|
|
||||||
b.WriteString("\nDo you mean ")
|
|
||||||
b.WriteString(hint)
|
|
||||||
b.WriteString("?")
|
|
||||||
}
|
|
||||||
b.WriteString("\n\n")
|
|
||||||
b.WriteString("Run `godoxy help` for available commands.")
|
|
||||||
return errors.New(b.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func findEndpointAlias(args []string) (*Endpoint, int) {
|
|
||||||
var best *Endpoint
|
|
||||||
bestLen := -1
|
|
||||||
for i := range generatedEndpoints {
|
|
||||||
alias := aliasCommandPath(generatedEndpoints[i])
|
|
||||||
if len(alias) == 0 || len(alias) > len(args) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ok := true
|
|
||||||
for j, tok := range alias {
|
|
||||||
if args[j] != tok {
|
|
||||||
ok = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ok && len(alias) > bestLen {
|
|
||||||
best = &generatedEndpoints[i]
|
|
||||||
bestLen = len(alias)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best, bestLen
|
|
||||||
}
|
|
||||||
|
|
||||||
func aliasCommandPath(ep Endpoint) []string {
|
|
||||||
rawPath := strings.TrimPrefix(ep.Path, "/api/v1/")
|
|
||||||
rawPath = strings.Trim(rawPath, "/")
|
|
||||||
if rawPath == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
parts := strings.Split(rawPath, "/")
|
|
||||||
if len(parts) == 1 {
|
|
||||||
if isPathParam(parts[0]) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []string{toKebabToken(parts[0])}
|
|
||||||
}
|
|
||||||
if isPathParam(parts[0]) || isPathParam(parts[1]) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []string{toKebabToken(parts[0]), toKebabToken(parts[1])}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPathParam(s string) bool {
|
|
||||||
return strings.HasPrefix(s, "{") || strings.HasPrefix(s, ":")
|
|
||||||
}
|
|
||||||
|
|
||||||
func toKebabToken(s string) string {
|
|
||||||
s = strings.ReplaceAll(s, "_", "-")
|
|
||||||
return strings.ToLower(strings.Trim(s, "-"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasGroup(group string) bool {
|
|
||||||
for _, ep := range generatedEndpoints {
|
|
||||||
if len(ep.CommandPath) > 1 && ep.CommandPath[0] == group {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func nearestCommand(input string) string {
|
|
||||||
commands := make([]string, 0, len(generatedEndpoints))
|
|
||||||
for _, ep := range generatedEndpoints {
|
|
||||||
commands = append(commands, strings.Join(ep.CommandPath, " "))
|
|
||||||
}
|
|
||||||
return nearestByDistance(input, commands)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nearestForGroup(group, input string) string {
|
|
||||||
choiceSet := map[string]struct{}{}
|
|
||||||
for _, ep := range generatedEndpoints {
|
|
||||||
if len(ep.CommandPath) < 2 || ep.CommandPath[0] != group {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
choiceSet[ep.CommandPath[1]] = struct{}{}
|
|
||||||
alias := aliasCommandPath(ep)
|
|
||||||
if len(alias) == 2 && alias[0] == group {
|
|
||||||
choiceSet[alias[1]] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
choices := make([]string, 0, len(choiceSet))
|
|
||||||
for choice := range choiceSet {
|
|
||||||
choices = append(choices, choice)
|
|
||||||
}
|
|
||||||
if len(choices) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return group + " " + nearestByDistance(input, choices)
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatGroupHelp(group string) string {
|
|
||||||
commands := make([]Endpoint, 0)
|
|
||||||
for _, ep := range generatedEndpoints {
|
|
||||||
if len(ep.CommandPath) > 1 && ep.CommandPath[0] == group {
|
|
||||||
commands = append(commands, ep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Slice(commands, func(i, j int) bool {
|
|
||||||
return strings.Join(commands[i].CommandPath, " ") < strings.Join(commands[j].CommandPath, " ")
|
|
||||||
})
|
|
||||||
maxWidth := 0
|
|
||||||
for _, ep := range commands {
|
|
||||||
cmd := strings.Join(ep.CommandPath, " ")
|
|
||||||
if len(cmd) > maxWidth {
|
|
||||||
maxWidth = len(cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var b strings.Builder
|
|
||||||
fmt.Fprintf(&b, "Available subcommands for %s:\n", group)
|
|
||||||
for _, ep := range commands {
|
|
||||||
cmd := strings.Join(ep.CommandPath, " ")
|
|
||||||
fmt.Fprintf(&b, " %-*s %s\n", maxWidth, cmd, ep.Summary)
|
|
||||||
}
|
|
||||||
return strings.TrimRight(b.String(), "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatEndpointHelp(ep Endpoint) string {
|
|
||||||
cmd := "godoxy " + strings.Join(ep.CommandPath, " ")
|
|
||||||
var b strings.Builder
|
|
||||||
fmt.Fprintf(&b, "Usage: %s [flags]\n", cmd)
|
|
||||||
if ep.Summary != "" {
|
|
||||||
fmt.Fprintf(&b, "Summary: %s\n", ep.Summary)
|
|
||||||
}
|
|
||||||
if alias := aliasCommandPath(ep); len(alias) > 0 && strings.Join(alias, " ") != strings.Join(ep.CommandPath, " ") {
|
|
||||||
fmt.Fprintf(&b, "Alias: godoxy %s\n", strings.Join(alias, " "))
|
|
||||||
}
|
|
||||||
params := make([]Param, 0, len(ep.Params))
|
|
||||||
params = append(params, ep.Params...)
|
|
||||||
if ep.IsWebSocket {
|
|
||||||
params = append(params, Param{
|
|
||||||
FlagName: "ws",
|
|
||||||
Type: "boolean",
|
|
||||||
Description: "use websocket",
|
|
||||||
Required: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(params) == 0 {
|
|
||||||
return strings.TrimRight(b.String(), "\n")
|
|
||||||
}
|
|
||||||
b.WriteString("Flags:\n")
|
|
||||||
maxWidth := 0
|
|
||||||
flagNames := make([]string, 0, len(params))
|
|
||||||
for _, p := range params {
|
|
||||||
name := "--" + p.FlagName
|
|
||||||
if p.Required {
|
|
||||||
name += " (required)"
|
|
||||||
}
|
|
||||||
flagNames = append(flagNames, name)
|
|
||||||
if len(name) > maxWidth {
|
|
||||||
maxWidth = len(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, p := range params {
|
|
||||||
desc := p.Description
|
|
||||||
if desc == "" {
|
|
||||||
desc = p.In + " " + p.Type
|
|
||||||
}
|
|
||||||
fmt.Fprintf(&b, " %-*s %s\n", maxWidth, flagNames[i], desc)
|
|
||||||
}
|
|
||||||
return strings.TrimRight(b.String(), "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func nearestByDistance(input string, choices []string) string {
|
|
||||||
if len(choices) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
nearest := choices[0]
|
|
||||||
minDistance := levenshteinDistance(input, nearest)
|
|
||||||
for _, choice := range choices[1:] {
|
|
||||||
d := levenshteinDistance(input, choice)
|
|
||||||
if d < minDistance {
|
|
||||||
minDistance = d
|
|
||||||
nearest = choice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nearest
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:intrange
|
|
||||||
func levenshteinDistance(a, b string) int {
|
|
||||||
if a == b {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if len(a) == 0 {
|
|
||||||
return len(b)
|
|
||||||
}
|
|
||||||
if len(b) == 0 {
|
|
||||||
return len(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
v0 := make([]int, len(b)+1)
|
|
||||||
v1 := make([]int, len(b)+1)
|
|
||||||
|
|
||||||
for i := 0; i <= len(b); i++ {
|
|
||||||
v0[i] = i
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(a); i++ {
|
|
||||||
v1[0] = i + 1
|
|
||||||
|
|
||||||
for j := 0; j < len(b); j++ {
|
|
||||||
cost := 0
|
|
||||||
if a[i] != b[j] {
|
|
||||||
cost = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
v1[j+1] = min3(v1[j]+1, v0[j+1]+1, v0[j]+cost)
|
|
||||||
}
|
|
||||||
|
|
||||||
for j := 0; j <= len(b); j++ {
|
|
||||||
v0[j] = v1[j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return v1[len(b)]
|
|
||||||
}
|
|
||||||
|
|
||||||
func min3(a, b, c int) int {
|
|
||||||
if a < b && a < c {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
if b < a && b < c {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
@@ -1,366 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"go/format"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
type swaggerSpec struct {
|
|
||||||
BasePath string `json:"basePath"`
|
|
||||||
Paths map[string]map[string]operation `json:"paths"`
|
|
||||||
Definitions map[string]definition `json:"definitions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type operation struct {
|
|
||||||
OperationID string `json:"operationId"`
|
|
||||||
Summary string `json:"summary"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
Parameters []parameter `json:"parameters"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type parameter struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
In string `json:"in"`
|
|
||||||
Required bool `json:"required"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Schema *schemaRef `json:"schema"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type schemaRef struct {
|
|
||||||
Ref string `json:"$ref"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type definition struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Required []string `json:"required"`
|
|
||||||
Properties map[string]definition `json:"properties"`
|
|
||||||
Items *definition `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type endpoint struct {
|
|
||||||
CommandPath []string
|
|
||||||
Method string
|
|
||||||
Path string
|
|
||||||
Summary string
|
|
||||||
IsWebSocket bool
|
|
||||||
Params []param
|
|
||||||
}
|
|
||||||
|
|
||||||
type param struct {
|
|
||||||
FlagName string
|
|
||||||
Name string
|
|
||||||
In string
|
|
||||||
Type string
|
|
||||||
Required bool
|
|
||||||
Description string
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
root := filepath.Join("..", "..")
|
|
||||||
inPath := filepath.Join(root, "internal", "api", "v1", "docs", "swagger.json")
|
|
||||||
outPath := "generated_commands.go"
|
|
||||||
|
|
||||||
raw, err := os.ReadFile(inPath)
|
|
||||||
must(err)
|
|
||||||
|
|
||||||
var spec swaggerSpec
|
|
||||||
must(json.Unmarshal(raw, &spec))
|
|
||||||
|
|
||||||
eps := buildEndpoints(spec)
|
|
||||||
must(writeGenerated(outPath, eps))
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildEndpoints(spec swaggerSpec) []endpoint {
|
|
||||||
byCommand := map[string]endpoint{}
|
|
||||||
|
|
||||||
pathKeys := make([]string, 0, len(spec.Paths))
|
|
||||||
for p := range spec.Paths {
|
|
||||||
pathKeys = append(pathKeys, p)
|
|
||||||
}
|
|
||||||
sort.Strings(pathKeys)
|
|
||||||
|
|
||||||
for _, p := range pathKeys {
|
|
||||||
methodMap := spec.Paths[p]
|
|
||||||
methods := make([]string, 0, len(methodMap))
|
|
||||||
for m := range methodMap {
|
|
||||||
methods = append(methods, strings.ToUpper(m))
|
|
||||||
}
|
|
||||||
sort.Strings(methods)
|
|
||||||
|
|
||||||
for _, method := range methods {
|
|
||||||
op := methodMap[strings.ToLower(method)]
|
|
||||||
if op.OperationID == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ep := endpoint{
|
|
||||||
CommandPath: commandPathFromOp(p, op.OperationID),
|
|
||||||
Method: method,
|
|
||||||
Path: ensureSlash(spec.BasePath) + normalizePath(p),
|
|
||||||
Summary: op.Summary,
|
|
||||||
IsWebSocket: hasTag(op.Tags, "websocket"),
|
|
||||||
Params: collectParams(spec, op),
|
|
||||||
}
|
|
||||||
key := strings.Join(ep.CommandPath, " ")
|
|
||||||
if existing, ok := byCommand[key]; ok {
|
|
||||||
if betterEndpoint(ep, existing) {
|
|
||||||
byCommand[key] = ep
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
byCommand[key] = ep
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make([]endpoint, 0, len(byCommand))
|
|
||||||
for _, ep := range byCommand {
|
|
||||||
out = append(out, ep)
|
|
||||||
}
|
|
||||||
sort.Slice(out, func(i, j int) bool {
|
|
||||||
ai := strings.Join(out[i].CommandPath, " ")
|
|
||||||
aj := strings.Join(out[j].CommandPath, " ")
|
|
||||||
return ai < aj
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func commandPathFromOp(path, opID string) []string {
|
|
||||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
||||||
if len(parts) == 0 {
|
|
||||||
return []string{toKebab(opID)}
|
|
||||||
}
|
|
||||||
if len(parts) == 1 {
|
|
||||||
return []string{toKebab(parts[0])}
|
|
||||||
}
|
|
||||||
group := toKebab(parts[0])
|
|
||||||
name := toKebab(opID)
|
|
||||||
if name == group {
|
|
||||||
name = "get"
|
|
||||||
}
|
|
||||||
if group == "v1" {
|
|
||||||
return []string{name}
|
|
||||||
}
|
|
||||||
return []string{group, name}
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectParams(spec swaggerSpec, op operation) []param {
|
|
||||||
params := make([]param, 0)
|
|
||||||
for _, p := range op.Parameters {
|
|
||||||
switch p.In {
|
|
||||||
case "body":
|
|
||||||
if p.Schema != nil && p.Schema.Ref != "" {
|
|
||||||
defName := strings.TrimPrefix(p.Schema.Ref, "#/definitions/")
|
|
||||||
params = append(params, bodyParamsFromDef(spec.Definitions[defName])...)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
params = append(params, param{
|
|
||||||
FlagName: toKebab(p.Name),
|
|
||||||
Name: p.Name,
|
|
||||||
In: "body",
|
|
||||||
Type: defaultType(p.Type),
|
|
||||||
Required: p.Required,
|
|
||||||
Description: p.Description,
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
params = append(params, param{
|
|
||||||
FlagName: toKebab(p.Name),
|
|
||||||
Name: p.Name,
|
|
||||||
In: p.In,
|
|
||||||
Type: defaultType(p.Type),
|
|
||||||
Required: p.Required,
|
|
||||||
Description: p.Description,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deduplicate by flag name, prefer required entries.
|
|
||||||
byFlag := map[string]param{}
|
|
||||||
for _, p := range params {
|
|
||||||
if cur, ok := byFlag[p.FlagName]; ok {
|
|
||||||
if !cur.Required && p.Required {
|
|
||||||
byFlag[p.FlagName] = p
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
byFlag[p.FlagName] = p
|
|
||||||
}
|
|
||||||
out := make([]param, 0, len(byFlag))
|
|
||||||
for _, p := range byFlag {
|
|
||||||
out = append(out, p)
|
|
||||||
}
|
|
||||||
sort.Slice(out, func(i, j int) bool {
|
|
||||||
if out[i].In != out[j].In {
|
|
||||||
return out[i].In < out[j].In
|
|
||||||
}
|
|
||||||
return out[i].FlagName < out[j].FlagName
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func bodyParamsFromDef(def definition) []param {
|
|
||||||
if def.Type != "object" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
requiredSet := map[string]struct{}{}
|
|
||||||
for _, name := range def.Required {
|
|
||||||
requiredSet[name] = struct{}{}
|
|
||||||
}
|
|
||||||
keys := make([]string, 0, len(def.Properties))
|
|
||||||
for k := range def.Properties {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
out := make([]param, 0, len(keys))
|
|
||||||
for _, k := range keys {
|
|
||||||
prop := def.Properties[k]
|
|
||||||
_, required := requiredSet[k]
|
|
||||||
t := defaultType(prop.Type)
|
|
||||||
if prop.Type == "array" {
|
|
||||||
t = "array"
|
|
||||||
}
|
|
||||||
if prop.Type == "object" {
|
|
||||||
t = "object"
|
|
||||||
}
|
|
||||||
out = append(out, param{
|
|
||||||
FlagName: toKebab(k),
|
|
||||||
Name: k,
|
|
||||||
In: "body",
|
|
||||||
Type: t,
|
|
||||||
Required: required,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func betterEndpoint(a, b endpoint) bool {
|
|
||||||
// Prefer GET, then fewer path params, then shorter path.
|
|
||||||
if a.Method == "GET" && b.Method != "GET" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if a.Method != "GET" && b.Method == "GET" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ac := countPathParams(a.Path)
|
|
||||||
bc := countPathParams(b.Path)
|
|
||||||
if ac != bc {
|
|
||||||
return ac < bc
|
|
||||||
}
|
|
||||||
return len(a.Path) < len(b.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func countPathParams(path string) int {
|
|
||||||
count := 0
|
|
||||||
for _, seg := range strings.Split(path, "/") {
|
|
||||||
if strings.HasPrefix(seg, "{") || strings.HasPrefix(seg, ":") {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePath(p string) string {
|
|
||||||
parts := strings.Split(p, "/")
|
|
||||||
for i, part := range parts {
|
|
||||||
if strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") {
|
|
||||||
name := strings.TrimSuffix(strings.TrimPrefix(part, "{"), "}")
|
|
||||||
parts[i] = "{" + name + "}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Join(parts, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasTag(tags []string, want string) bool {
|
|
||||||
for _, t := range tags {
|
|
||||||
if strings.EqualFold(t, want) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeGenerated(outPath string, eps []endpoint) error {
|
|
||||||
var b bytes.Buffer
|
|
||||||
b.WriteString("// Code generated by cmd/cli/gen. DO NOT EDIT.\n")
|
|
||||||
b.WriteString("package main\n\n")
|
|
||||||
b.WriteString("var generatedEndpoints = []Endpoint{\n")
|
|
||||||
for _, ep := range eps {
|
|
||||||
b.WriteString("\t{\n")
|
|
||||||
fmt.Fprintf(&b, "\t\tCommandPath: %#v,\n", ep.CommandPath)
|
|
||||||
fmt.Fprintf(&b, "\t\tMethod: %q,\n", ep.Method)
|
|
||||||
fmt.Fprintf(&b, "\t\tPath: %q,\n", ep.Path)
|
|
||||||
fmt.Fprintf(&b, "\t\tSummary: %q,\n", ep.Summary)
|
|
||||||
fmt.Fprintf(&b, "\t\tIsWebSocket: %t,\n", ep.IsWebSocket)
|
|
||||||
b.WriteString("\t\tParams: []Param{\n")
|
|
||||||
for _, p := range ep.Params {
|
|
||||||
b.WriteString("\t\t\t{\n")
|
|
||||||
fmt.Fprintf(&b, "\t\t\t\tFlagName: %q,\n", p.FlagName)
|
|
||||||
fmt.Fprintf(&b, "\t\t\t\tName: %q,\n", p.Name)
|
|
||||||
fmt.Fprintf(&b, "\t\t\t\tIn: %q,\n", p.In)
|
|
||||||
fmt.Fprintf(&b, "\t\t\t\tType: %q,\n", p.Type)
|
|
||||||
fmt.Fprintf(&b, "\t\t\t\tRequired: %t,\n", p.Required)
|
|
||||||
fmt.Fprintf(&b, "\t\t\t\tDescription: %q,\n", p.Description)
|
|
||||||
b.WriteString("\t\t\t},\n")
|
|
||||||
}
|
|
||||||
b.WriteString("\t\t},\n")
|
|
||||||
b.WriteString("\t},\n")
|
|
||||||
}
|
|
||||||
b.WriteString("}\n")
|
|
||||||
formatted, err := format.Source(b.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("format generated source: %w", err)
|
|
||||||
}
|
|
||||||
return os.WriteFile(outPath, formatted, 0o644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureSlash(s string) string {
|
|
||||||
if strings.HasPrefix(s, "/") {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return "/" + s
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultType(t string) string {
|
|
||||||
switch t {
|
|
||||||
case "integer", "number", "boolean", "array", "object", "string":
|
|
||||||
return t
|
|
||||||
default:
|
|
||||||
return "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toKebab(s string) string {
|
|
||||||
if s == "" {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
s = strings.ReplaceAll(s, "_", "-")
|
|
||||||
s = strings.ReplaceAll(s, ".", "-")
|
|
||||||
var out []rune
|
|
||||||
for i, r := range s {
|
|
||||||
if unicode.IsUpper(r) {
|
|
||||||
if i > 0 && out[len(out)-1] != '-' {
|
|
||||||
out = append(out, '-')
|
|
||||||
}
|
|
||||||
out = append(out, unicode.ToLower(r))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, unicode.ToLower(r))
|
|
||||||
}
|
|
||||||
res := strings.Trim(string(out), "-")
|
|
||||||
for strings.Contains(res, "--") {
|
|
||||||
res = strings.ReplaceAll(res, "--", "-")
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func must(err error) {
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
module github.com/yusing/godoxy/cli
|
|
||||||
|
|
||||||
go 1.26.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/gorilla/websocket v1.5.3
|
|
||||||
github.com/yusing/goutils v0.7.0
|
|
||||||
)
|
|
||||||
|
|
||||||
replace github.com/yusing/goutils => ../../goutils
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if err := run(os.Args[1:]); err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, "error:", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
type Param struct {
|
|
||||||
FlagName string
|
|
||||||
Name string
|
|
||||||
In string
|
|
||||||
Type string
|
|
||||||
Required bool
|
|
||||||
Description string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Endpoint struct {
|
|
||||||
CommandPath []string
|
|
||||||
Method string
|
|
||||||
Path string
|
|
||||||
Summary string
|
|
||||||
IsWebSocket bool
|
|
||||||
Params []Param
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/yusing/godoxy/internal/api"
|
"github.com/yusing/godoxy/internal/api"
|
||||||
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
||||||
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
|
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
|
||||||
@@ -129,31 +128,25 @@ func listenDebugServer() {
|
|||||||
mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "image/svg+xml")
|
w.Header().Set("Content-Type", "image/svg+xml")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
fmt.Fprint(w, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`)
|
w.Write([]byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`))
|
||||||
})
|
})
|
||||||
|
|
||||||
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
|
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
|
||||||
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
|
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
|
||||||
apiHandler := newAPIHandler(mux)
|
apiHandler := newApiHandler(mux)
|
||||||
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
|
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
|
||||||
|
|
||||||
mux.Finalize()
|
mux.Finalize()
|
||||||
|
|
||||||
go func() {
|
go http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
//nolint:gosec
|
w.Header().Set("Pragma", "no-cache")
|
||||||
err := http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
w.Header().Set("Pragma", "no-cache")
|
w.Header().Set("Expires", "0")
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
mux.mux.ServeHTTP(w, r)
|
||||||
w.Header().Set("Expires", "0")
|
}))
|
||||||
mux.mux.ServeHTTP(w, r)
|
|
||||||
}))
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Error starting debug server")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAPIHandler(debugMux *debugMux) *gin.Engine {
|
func newApiHandler(debugMux *debugMux) *gin.Engine {
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(api.ErrorHandler())
|
r.Use(api.ErrorHandler())
|
||||||
r.Use(api.ErrorLoggingMiddleware())
|
r.Use(api.ErrorLoggingMiddleware())
|
||||||
@@ -188,6 +181,7 @@ func newAPIHandler(debugMux *debugMux) *gin.Engine {
|
|||||||
registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon)
|
registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon)
|
||||||
registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health)
|
registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health)
|
||||||
registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons)
|
registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons)
|
||||||
|
registerGinRoute(v1, "POST", "Config reload", "/reload", apiV1.Reload)
|
||||||
registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats)
|
registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats)
|
||||||
|
|
||||||
route := v1.Group("/route")
|
route := v1.Group("/route")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.26.0-alpine AS builder
|
FROM golang:1.25.6-alpine AS builder
|
||||||
|
|
||||||
HEALTHCHECK NONE
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module github.com/yusing/godoxy/cmd/h2c_test_server
|
module github.com/yusing/godoxy/cmd/h2c_test_server
|
||||||
|
|
||||||
go 1.26.0
|
go 1.25.6
|
||||||
|
|
||||||
require golang.org/x/net v0.50.0
|
require golang.org/x/net v0.49.0
|
||||||
|
|
||||||
require golang.org/x/text v0.34.0 // indirect
|
require golang.org/x/text v0.33.0 // indirect
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
|
|||||||
31
cmd/main.go
31
cmd/main.go
@@ -1,12 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/godoxy/internal/api"
|
||||||
"github.com/yusing/godoxy/internal/auth"
|
"github.com/yusing/godoxy/internal/auth"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
"github.com/yusing/godoxy/internal/config"
|
"github.com/yusing/godoxy/internal/config"
|
||||||
@@ -14,8 +14,12 @@ import (
|
|||||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||||
"github.com/yusing/godoxy/internal/logging"
|
"github.com/yusing/godoxy/internal/logging"
|
||||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||||
|
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||||
|
"github.com/yusing/godoxy/internal/metrics/uptime"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||||
"github.com/yusing/godoxy/internal/route/rules"
|
"github.com/yusing/godoxy/internal/route/rules"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
"github.com/yusing/goutils/server"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
"github.com/yusing/goutils/version"
|
"github.com/yusing/goutils/version"
|
||||||
)
|
)
|
||||||
@@ -47,6 +51,7 @@ func main() {
|
|||||||
parallel(
|
parallel(
|
||||||
dnsproviders.InitProviders,
|
dnsproviders.InitProviders,
|
||||||
iconlist.InitCache,
|
iconlist.InitCache,
|
||||||
|
systeminfo.Poller.Start,
|
||||||
middleware.LoadComposeFiles,
|
middleware.LoadComposeFiles,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,19 +66,35 @@ func main() {
|
|||||||
|
|
||||||
err := config.Load()
|
err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if criticalErr, ok := errors.AsType[config.CriticalError](err); ok {
|
gperr.LogWarn("errors in config", err)
|
||||||
log.Fatal().Err(criticalErr).Msg("critical error in config")
|
|
||||||
}
|
|
||||||
log.Warn().Err(err).Msg("errors in config")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.StartProxyServers()
|
||||||
|
|
||||||
if err := auth.Initialize(); err != nil {
|
if err := auth.Initialize(); err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||||
}
|
}
|
||||||
rules.InitAuthHandler(auth.AuthOrProceed)
|
rules.InitAuthHandler(auth.AuthOrProceed)
|
||||||
|
|
||||||
|
// API Handler needs to start after auth is initialized.
|
||||||
|
server.StartServer(task.RootTask("api_server", false), server.Options{
|
||||||
|
Name: "api",
|
||||||
|
HTTPAddr: common.APIHTTPAddr,
|
||||||
|
Handler: api.NewHandler(true),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Local API Handler is used for unauthenticated access.
|
||||||
|
if common.LocalAPIHTTPAddr != "" {
|
||||||
|
server.StartServer(task.RootTask("local_api_server", false), server.Options{
|
||||||
|
Name: "local_api",
|
||||||
|
HTTPAddr: common.LocalAPIHTTPAddr,
|
||||||
|
Handler: api.NewHandler(false),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
listenDebugServer()
|
listenDebugServer()
|
||||||
|
|
||||||
|
uptime.Poller.Start()
|
||||||
config.WatchChanges()
|
config.WatchChanges()
|
||||||
|
|
||||||
close(done)
|
close(done)
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ services:
|
|||||||
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
||||||
read_only: true
|
read_only: true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /tmp:rw
|
- /app/.next/cache # next image caching
|
||||||
- /app/node_modules/.cache:rw
|
|
||||||
# for lite variant, do not change uid/gid
|
# for lite variant, do not change uid/gid
|
||||||
# - /var/cache/nginx:uid=101,gid=101
|
# - /var/cache/nginx:uid=101,gid=101
|
||||||
# - /run:uid=101,gid=101
|
# - /run:uid=101,gid=101
|
||||||
|
|||||||
86
go.mod
86
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/yusing/godoxy
|
module github.com/yusing/godoxy
|
||||||
|
|
||||||
go 1.26.0
|
go 1.25.6
|
||||||
|
|
||||||
exclude (
|
exclude (
|
||||||
github.com/moby/moby/api v1.53.0 // allow older daemon versions
|
github.com/moby/moby/api v1.53.0 // allow older daemon versions
|
||||||
@@ -21,23 +21,22 @@ replace (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
|
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // backoff for retrying operations
|
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
|
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||||
github.com/gin-gonic/gin v1.11.0 // api server
|
github.com/gin-gonic/gin v1.11.0 // api server
|
||||||
github.com/go-acme/lego/v4 v4.32.0 // acme client
|
github.com/go-acme/lego/v4 v4.31.0 // acme client
|
||||||
github.com/go-playground/validator/v10 v10.30.1 // validator
|
github.com/go-playground/validator/v10 v10.30.1 // validator
|
||||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||||
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||||
github.com/gotify/server/v2 v2.9.0 // reference the Message struct for json response
|
github.com/gotify/server/v2 v2.8.0 // reference the Message struct for json response
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||||
github.com/pires/go-proxyproto v0.11.0 // proxy protocol support
|
github.com/pires/go-proxyproto v0.9.2 // proxy protocol support
|
||||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 // lock free map for concurrent operations
|
github.com/puzpuzpuz/xsync/v4 v4.4.0 // lock free map for concurrent operations
|
||||||
github.com/rs/zerolog v1.34.0 // logging
|
github.com/rs/zerolog v1.34.0 // logging
|
||||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||||
golang.org/x/crypto v0.48.0 // encrypting password with bcrypt
|
golang.org/x/crypto v0.47.0 // encrypting password with bcrypt
|
||||||
golang.org/x/net v0.50.0 // HTTP header utilities
|
golang.org/x/net v0.49.0 // HTTP header utilities
|
||||||
golang.org/x/oauth2 v0.35.0 // oauth2 authentication
|
golang.org/x/oauth2 v0.34.0 // oauth2 authentication
|
||||||
golang.org/x/sync v0.19.0 // errgroup and singleflight for concurrent operations
|
golang.org/x/sync v0.19.0 // errgroup and singleflight for concurrent operations
|
||||||
golang.org/x/time v0.14.0 // time utilities
|
golang.org/x/time v0.14.0 // time utilities
|
||||||
)
|
)
|
||||||
@@ -45,30 +44,30 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||||
github.com/bytedance/sonic v1.15.0 // fast json parsing
|
github.com/bytedance/sonic v1.15.0 // fast json parsing
|
||||||
github.com/docker/cli v29.2.1+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
github.com/docker/cli v29.2.0+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||||
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
|
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 // jwt authentication
|
github.com/golang-jwt/jwt/v5 v5.3.1 // jwt authentication
|
||||||
github.com/luthermonson/go-proxmox v0.4.0 // proxmox API client
|
github.com/luthermonson/go-proxmox v0.3.2 // proxmox API client
|
||||||
github.com/moby/moby/api v1.52.0 // docker API
|
github.com/moby/moby/api v1.52.0 // docker API
|
||||||
github.com/moby/moby/client v0.2.1 // docker client
|
github.com/moby/moby/client v0.2.1 // docker client
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
|
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
|
||||||
github.com/quic-go/quic-go v0.59.0 // http3 support
|
github.com/quic-go/quic-go v0.59.0 // http3 support
|
||||||
github.com/shirou/gopsutil/v4 v4.26.1 // system information
|
github.com/shirou/gopsutil/v4 v4.25.12 // system information
|
||||||
github.com/spf13/afero v1.15.0 // afero for file system operations
|
github.com/spf13/afero v1.15.0 // afero for file system operations
|
||||||
github.com/stretchr/testify v1.11.1 // testing framework
|
github.com/stretchr/testify v1.11.1 // testing framework
|
||||||
github.com/valyala/fasthttp v1.69.0 // fast http for health check
|
github.com/valyala/fasthttp v1.69.0 // fast http for health check
|
||||||
github.com/yusing/ds v0.4.1 // data structures and algorithms
|
github.com/yusing/ds v0.4.1 // data structures and algorithms
|
||||||
github.com/yusing/godoxy/agent v0.0.0-20260218101334-add7884a365e
|
github.com/yusing/godoxy/agent v0.0.0-20260129101716-0f13004ad6ba
|
||||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260218101334-add7884a365e
|
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260129101716-0f13004ad6ba
|
||||||
github.com/yusing/gointernals v0.2.0
|
github.com/yusing/gointernals v0.1.16
|
||||||
github.com/yusing/goutils v0.7.0
|
github.com/yusing/goutils v0.7.0
|
||||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260218062549-0b0fa3a059ec
|
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468
|
||||||
github.com/yusing/goutils/http/websocket v0.0.0-20260218062549-0b0fa3a059ec
|
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468
|
||||||
github.com/yusing/goutils/server v0.0.0-20260218062549-0b0fa3a059ec
|
github.com/yusing/goutils/server v0.0.0-20260129081554-24e52ede7468
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/auth v0.18.2 // indirect
|
cloud.google.com/go/auth v0.18.1 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||||
@@ -90,7 +89,7 @@ require (
|
|||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/ebitengine/purego v0.9.1 // indirect
|
github.com/ebitengine/purego v0.9.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
@@ -99,8 +98,8 @@ require (
|
|||||||
github.com/gofrs/flock v0.13.0 // indirect
|
github.com/gofrs/flock v0.13.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||||
github.com/jinzhu/copier v0.4.0 // indirect
|
github.com/jinzhu/copier v0.4.0 // indirect
|
||||||
@@ -125,26 +124,26 @@ require (
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/samber/lo v1.52.0 // indirect
|
github.com/samber/lo v1.52.0 // indirect
|
||||||
github.com/samber/slog-common v0.20.0 // indirect
|
github.com/samber/slog-common v0.19.0 // indirect
|
||||||
github.com/samber/slog-zerolog/v2 v2.9.1 // indirect
|
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/sony/gobreaker v1.0.0 // indirect
|
github.com/sony/gobreaker v1.0.0 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
|
||||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0
|
go.uber.org/atomic v1.11.0
|
||||||
go.uber.org/ratelimit v0.3.1 // indirect
|
go.uber.org/ratelimit v0.3.1 // indirect
|
||||||
golang.org/x/mod v0.33.0 // indirect
|
golang.org/x/mod v0.32.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
golang.org/x/tools v0.42.0 // indirect
|
golang.org/x/tools v0.41.0 // indirect
|
||||||
google.golang.org/api v0.267.0 // indirect
|
google.golang.org/api v0.263.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||||
google.golang.org/grpc v1.79.1 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.1 // indirect
|
gopkg.in/ini.v1 v1.67.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
@@ -156,6 +155,7 @@ require (
|
|||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/boombuler/barcode v1.1.0 // indirect
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
@@ -165,20 +165,20 @@ require (
|
|||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||||
github.com/go-resty/resty/v2 v2.17.2 // indirect
|
github.com/go-resty/resty/v2 v2.17.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/google/go-querystring v1.2.0 // indirect
|
github.com/google/go-querystring v1.2.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.3 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||||
github.com/linode/linodego v1.65.0 // indirect
|
github.com/linode/linodego v1.64.0 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 // indirect
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 // indirect
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
github.com/pion/dtls/v3 v3.1.2 // indirect
|
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||||
github.com/pion/logging v0.2.4 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
@@ -190,7 +190,7 @@ require (
|
|||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/vultr/govultr/v3 v3.27.0 // indirect
|
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
golang.org/x/arch v0.24.0 // indirect
|
golang.org/x/arch v0.23.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
152
go.sum
152
go.sum
@@ -1,5 +1,5 @@
|
|||||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
||||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
@@ -76,8 +76,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||||
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
|
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
||||||
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@@ -94,14 +94,14 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
|||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
|
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||||
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
|
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
@@ -122,8 +122,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
|
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
|
||||||
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
@@ -151,14 +151,14 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
|||||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gotify/server/v2 v2.9.0 h1:2zRCl28wkq0oc6YNbyJS2n0dDOOVvOS3Oez5AG2ij54=
|
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||||
github.com/gotify/server/v2 v2.9.0/go.mod h1:249wwlUqHTr0QsiKARGtFVqds0pNLIMjYLinHyMACdQ=
|
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
|
||||||
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||||
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
@@ -177,8 +177,8 @@ github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/
|
|||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
||||||
@@ -191,12 +191,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
|||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
|
github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY=
|
||||||
github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
|
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
@@ -227,10 +227,10 @@ github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0
|
|||||||
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||||
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||||
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2 h1:OWijzl3nHUApvTivl+3+78dbBwmyEHOnb+W9m6ixGbk=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 h1:eMzyN+jGJbxG4ut278uwIsUo9XacXc711lFjhKnaUso=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2 h1:9LsjN/zaIN7H8JE61NHpbWhxF0UGY96+kMlk3g8OvGU=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 h1:t34IpOa+8NfmjkU8bdWtYrLrmr346/FGhu8FlpJDQok=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.2/go.mod h1:32vZH06TuwZSn+IDMO1qcDvC2vHVlzUALCwXGWPA+dc=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0/go.mod h1:p95/OxVsdx71I2Qrck1GtIS87sRxcTRKXzUi5nWm9NY=
|
||||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -245,14 +245,14 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
|
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||||
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
|
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||||
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
|
||||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -278,10 +278,10 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
|||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
|
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||||
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
|
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||||
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
|
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
|
||||||
github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k=
|
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
|
||||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
@@ -320,8 +320,8 @@ github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZy
|
|||||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||||
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||||
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||||
github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
|
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
|
||||||
github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
@@ -329,49 +329,49 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
||||||
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||||
github.com/yusing/gointernals v0.2.0 h1:jyWB3kdUPkuU6s0r8QY/sS5h2WNBF4Kfisly8dtSVvg=
|
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||||
github.com/yusing/gointernals v0.2.0/go.mod h1:xGzNbPGMm5Z8kG0t4JYISMscw+gMQlgghkLxlgRZv5Y=
|
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||||
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
||||||
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
||||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
@@ -381,10 +381,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -412,8 +412,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -432,8 +432,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -442,21 +442,21 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
|
google.golang.org/api v0.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk=
|
||||||
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
|
||||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
2
goutils
2
goutils
Submodule goutils updated: 3be815cb6e...52ea531e95
@@ -1,4 +1,4 @@
|
|||||||
# internal/acl
|
# ACL (Access Control List)
|
||||||
|
|
||||||
Access control at the TCP connection level with IP/CIDR, timezone, and country-based filtering.
|
Access control at the TCP connection level with IP/CIDR, timezone, and country-based filtering.
|
||||||
|
|
||||||
@@ -54,13 +54,13 @@ type Matchers []Matcher
|
|||||||
### Exported functions and methods
|
### Exported functions and methods
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (c *Config) Validate() error
|
func (c *Config) Validate() gperr.Error
|
||||||
```
|
```
|
||||||
|
|
||||||
Validates configuration and sets defaults. Must be called before `Start`.
|
Validates configuration and sets defaults. Must be called before `Start`.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (c *Config) Start(parent task.Parent) error
|
func (c *Config) Start(parent task.Parent) gperr.Error
|
||||||
```
|
```
|
||||||
|
|
||||||
Initializes the ACL, starts the logger and notification goroutines.
|
Initializes the ACL, starts the logger and notification goroutines.
|
||||||
@@ -169,14 +169,14 @@ Configuration is loaded from `config/config.yml` under the `acl` key.
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
acl:
|
acl:
|
||||||
default: "allow" # "allow" or "deny"
|
default: "allow" # "allow" or "deny"
|
||||||
allow_local: true # Allow private/loopback IPs
|
allow_local: true # Allow private/loopback IPs
|
||||||
log:
|
log:
|
||||||
log_allowed: false # Log allowed connections
|
log_allowed: false # Log allowed connections
|
||||||
notify:
|
notify:
|
||||||
to: ["gotify"] # Notification providers
|
to: ["gotify"] # Notification providers
|
||||||
interval: "1m" # Notification interval
|
interval: "1m" # Notification interval
|
||||||
include_allowed: false # Include allowed in notifications
|
include_allowed: false # Include allowed in notifications
|
||||||
```
|
```
|
||||||
|
|
||||||
### Hot-reloading
|
### Hot-reloading
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/yusing/godoxy/internal/maxmind"
|
"github.com/yusing/godoxy/internal/maxmind"
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
aclevents "github.com/yusing/goutils/events/acl"
|
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
@@ -67,16 +66,16 @@ type config struct {
|
|||||||
type checkCache struct {
|
type checkCache struct {
|
||||||
*maxmind.IPInfo
|
*maxmind.IPInfo
|
||||||
allow bool
|
allow bool
|
||||||
reason string
|
|
||||||
created time.Time
|
created time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type ipLog struct {
|
type ipLog struct {
|
||||||
info *maxmind.IPInfo
|
info *maxmind.IPInfo
|
||||||
allowed bool
|
allowed bool
|
||||||
reason string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ContextKey struct{}
|
||||||
|
|
||||||
const cacheTTL = 1 * time.Minute
|
const cacheTTL = 1 * time.Minute
|
||||||
|
|
||||||
func (c *checkCache) Expired() bool {
|
func (c *checkCache) Expired() bool {
|
||||||
@@ -90,7 +89,7 @@ const (
|
|||||||
ACLDeny = "deny"
|
ACLDeny = "deny"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Config) Validate() error {
|
func (c *Config) Validate() gperr.Error {
|
||||||
switch c.Default {
|
switch c.Default {
|
||||||
case "", ACLAllow:
|
case "", ACLAllow:
|
||||||
c.defaultAllow = true
|
c.defaultAllow = true
|
||||||
@@ -134,10 +133,7 @@ func (c *Config) Valid() bool {
|
|||||||
return c != nil && c.valErr == nil
|
return c != nil && c.valErr == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Start(parent task.Parent) error {
|
func (c *Config) Start(parent task.Parent) gperr.Error {
|
||||||
if c.valErr != nil {
|
|
||||||
return c.valErr
|
|
||||||
}
|
|
||||||
if c.Log != nil {
|
if c.Log != nil {
|
||||||
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -145,6 +141,9 @@ func (c *Config) Start(parent task.Parent) error {
|
|||||||
}
|
}
|
||||||
c.logger = logger
|
c.logger = logger
|
||||||
}
|
}
|
||||||
|
if c.valErr != nil {
|
||||||
|
return c.valErr
|
||||||
|
}
|
||||||
|
|
||||||
if c.needLogOrNotify() {
|
if c.needLogOrNotify() {
|
||||||
c.logNotifyCh = make(chan ipLog, 100)
|
c.logNotifyCh = make(chan ipLog, 100)
|
||||||
@@ -171,14 +170,13 @@ func (c *Config) Start(parent task.Parent) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool, reason string) {
|
func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
|
||||||
if common.ForceResolveCountry && info.City == nil {
|
if common.ForceResolveCountry && info.City == nil {
|
||||||
maxmind.LookupCity(info)
|
maxmind.LookupCity(info)
|
||||||
}
|
}
|
||||||
c.ipCache.Store(info.Str, &checkCache{
|
c.ipCache.Store(info.Str, &checkCache{
|
||||||
IPInfo: info,
|
IPInfo: info,
|
||||||
allow: allow,
|
allow: allow,
|
||||||
reason: reason,
|
|
||||||
created: time.Now(),
|
created: time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -215,26 +213,23 @@ func (c *Config) logNotifyLoop(parent task.Parent) {
|
|||||||
select {
|
select {
|
||||||
case <-parent.Context().Done():
|
case <-parent.Context().Done():
|
||||||
return
|
return
|
||||||
case req := <-c.logNotifyCh:
|
case log := <-c.logNotifyCh:
|
||||||
if c.logger != nil {
|
if c.logger != nil {
|
||||||
if !req.allowed || c.logAllowed {
|
if !log.allowed || c.logAllowed {
|
||||||
c.logger.LogACL(req.info, !req.allowed, req.reason)
|
c.logger.LogACL(log.info, !log.allowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.needNotify() {
|
if c.needNotify() {
|
||||||
if req.allowed {
|
if log.allowed {
|
||||||
if c.notifyAllowed {
|
if c.notifyAllowed {
|
||||||
c.allowedCount[req.info.Str]++
|
c.allowedCount[log.info.Str]++
|
||||||
c.totalAllowedCount++
|
c.totalAllowedCount++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.blockedCount[req.info.Str]++
|
c.blockedCount[log.info.Str]++
|
||||||
c.totalBlockedCount++
|
c.totalBlockedCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !req.allowed {
|
|
||||||
aclevents.Blocked(req.info.Str, req.reason)
|
|
||||||
}
|
|
||||||
case <-c.notifyTicker.C: // will never tick when notify is disabled
|
case <-c.notifyTicker.C: // will never tick when notify is disabled
|
||||||
total := len(c.allowedCount) + len(c.blockedCount)
|
total := len(c.allowedCount) + len(c.blockedCount)
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
@@ -266,9 +261,9 @@ func (c *Config) logNotifyLoop(parent task.Parent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// log and notify if needed
|
// log and notify if needed
|
||||||
func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool, reason string) {
|
func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool) {
|
||||||
if c.logNotifyCh != nil {
|
if c.logNotifyCh != nil {
|
||||||
c.logNotifyCh <- ipLog{info: info, allowed: allowed, reason: reason}
|
c.logNotifyCh <- ipLog{info: info, allowed: allowed}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,36 +278,30 @@ func (c *Config) IPAllowed(ip net.IP) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.allowLocal && ip.IsPrivate() {
|
if c.allowLocal && ip.IsPrivate() {
|
||||||
c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true, "allowed by allow_local rule")
|
c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
ipStr := ip.String()
|
ipStr := ip.String()
|
||||||
record, ok := c.ipCache.Load(ipStr)
|
record, ok := c.ipCache.Load(ipStr)
|
||||||
if ok && !record.Expired() {
|
if ok && !record.Expired() {
|
||||||
c.logAndNotify(record.IPInfo, record.allow, record.reason)
|
c.logAndNotify(record.IPInfo, record.allow)
|
||||||
return record.allow
|
return record.allow
|
||||||
}
|
}
|
||||||
|
|
||||||
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
|
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
|
||||||
if index := c.Deny.MatchedIndex(ipAndStr); index != -1 {
|
if c.Deny.Match(ipAndStr) {
|
||||||
reason := "blocked by deny rule: " + c.Deny[index].raw
|
c.logAndNotify(ipAndStr, false)
|
||||||
c.logAndNotify(ipAndStr, false, reason)
|
c.cacheRecord(ipAndStr, false)
|
||||||
c.cacheRecord(ipAndStr, false, reason)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if index := c.Allow.MatchedIndex(ipAndStr); index != -1 {
|
if c.Allow.Match(ipAndStr) {
|
||||||
reason := "allowed by allow rule: " + c.Allow[index].raw
|
c.logAndNotify(ipAndStr, true)
|
||||||
c.logAndNotify(ipAndStr, true, reason)
|
c.cacheRecord(ipAndStr, true)
|
||||||
c.cacheRecord(ipAndStr, true, reason)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
reason := "denied by default"
|
c.logAndNotify(ipAndStr, c.defaultAllow)
|
||||||
if c.defaultAllow {
|
c.cacheRecord(ipAndStr, c.defaultAllow)
|
||||||
reason = "allowed by default"
|
|
||||||
}
|
|
||||||
c.logAndNotify(ipAndStr, c.defaultAllow, reason)
|
|
||||||
c.cacheRecord(ipAndStr, c.defaultAllow, reason)
|
|
||||||
return c.defaultAllow
|
return c.defaultAllow
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package acl
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -39,9 +38,9 @@ var errMatcherFormat = gperr.Multiline().AddLines(
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errSyntax = errors.New("syntax error")
|
errSyntax = gperr.New("syntax error")
|
||||||
errInvalidIP = errors.New("invalid IP")
|
errInvalidIP = gperr.New("invalid IP")
|
||||||
errInvalidCIDR = errors.New("invalid CIDR")
|
errInvalidCIDR = gperr.New("invalid CIDR")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (matcher *Matcher) Parse(s string) error {
|
func (matcher *Matcher) Parse(s string) error {
|
||||||
@@ -83,15 +82,6 @@ func (matchers Matchers) Match(ip *maxmind.IPInfo) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (matchers Matchers) MatchedIndex(ip *maxmind.IPInfo) int {
|
|
||||||
for i, m := range matchers {
|
|
||||||
if m.match(ip) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (matchers Matchers) MarshalText() ([]byte, error) {
|
func (matchers Matchers) MarshalText() ([]byte, error) {
|
||||||
if len(matchers) == 0 {
|
if len(matchers) == 0 {
|
||||||
return []byte("[]"), nil
|
return []byte("[]"), nil
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TCPListener struct {
|
type TCPListener struct {
|
||||||
@@ -46,7 +44,6 @@ func (s *TCPListener) Accept() (net.Conn, error) {
|
|||||||
}
|
}
|
||||||
addr, ok := c.RemoteAddr().(*net.TCPAddr)
|
addr, ok := c.RemoteAddr().(*net.TCPAddr)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Error().Msgf("unexpected remote address type: %T, addr: %s", c.RemoteAddr(), c.RemoteAddr().String())
|
|
||||||
// Not a TCPAddr, drop
|
// Not a TCPAddr, drop
|
||||||
c.Close()
|
c.Close()
|
||||||
return noConn{}, nil
|
return noConn{}, nil
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package acl
|
|
||||||
|
|
||||||
import "net"
|
|
||||||
|
|
||||||
type ACL interface {
|
|
||||||
IPAllowed(ip net.IP) bool
|
|
||||||
WrapTCP(l net.Listener) net.Listener
|
|
||||||
WrapUDP(l net.PacketConn) net.PacketConn
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package acl
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type ContextKey struct{}
|
|
||||||
|
|
||||||
func SetCtx(ctx interface{ SetValue(key any, value any) }, acl ACL) {
|
|
||||||
ctx.SetValue(ContextKey{}, acl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromCtx(ctx context.Context) ACL {
|
|
||||||
if acl, ok := ctx.Value(ContextKey{}).(ACL); ok {
|
|
||||||
return acl
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UDPListener struct {
|
type UDPListener struct {
|
||||||
@@ -35,7 +33,6 @@ func (s *UDPListener) ReadFrom(p []byte) (int, net.Addr, error) {
|
|||||||
}
|
}
|
||||||
udpAddr, ok := addr.(*net.UDPAddr)
|
udpAddr, ok := addr.(*net.UDPAddr)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Error().Msgf("unexpected remote address type: %T, addr: %s", addr, addr.String())
|
|
||||||
// Not a UDPAddr, drop
|
// Not a UDPAddr, drop
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -55,7 +52,6 @@ func (s *UDPListener) WriteTo(p []byte, addr net.Addr) (int, error) {
|
|||||||
}
|
}
|
||||||
udpAddr, ok := addr.(*net.UDPAddr)
|
udpAddr, ok := addr.(*net.UDPAddr)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Error().Msgf("unexpected remote address type: %T, addr: %s", addr, addr.String())
|
|
||||||
// Not a UDPAddr, drop
|
// Not a UDPAddr, drop
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# internal/agentpool
|
# Agent Pool
|
||||||
|
|
||||||
Thread-safe pool for managing remote Docker agent connections.
|
Thread-safe pool for managing remote Docker agent connections.
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,7 @@ func newAgent(cfg *agent.AgentConfig) *Agent {
|
|||||||
if addr != agent.AgentHost+":443" {
|
if addr != agent.AgentHost+":443" {
|
||||||
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
||||||
}
|
}
|
||||||
dialer := &net.Dialer{
|
return net.DialTimeout("tcp", cfg.Addr, timeout)
|
||||||
Timeout: timeout,
|
|
||||||
}
|
|
||||||
return dialer.Dial("tcp", cfg.Addr)
|
|
||||||
},
|
},
|
||||||
TLSConfig: cfg.TLSConfig(),
|
TLSConfig: cfg.TLSConfig(),
|
||||||
ReadTimeout: 5 * time.Second,
|
ReadTimeout: 5 * time.Second,
|
||||||
|
|||||||
@@ -10,24 +10,24 @@ import (
|
|||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
agentPkg "github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/goutils/http/reverseproxy"
|
"github.com/yusing/goutils/http/reverseproxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (agent *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
func (cfg *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, method, agentPkg.APIBaseURL+endpoint, body)
|
req, err := http.NewRequestWithContext(ctx, method, agent.APIBaseURL+endpoint, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return agent.httpClient.Do(req)
|
return cfg.httpClient.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (agent *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) {
|
func (cfg *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) {
|
||||||
req.URL.Host = agentPkg.AgentHost
|
req.URL.Host = agent.AgentHost
|
||||||
req.URL.Scheme = "https"
|
req.URL.Scheme = "https"
|
||||||
req.URL.Path = agentPkg.APIEndpointBase + endpoint
|
req.URL.Path = agent.APIEndpointBase + endpoint
|
||||||
req.RequestURI = ""
|
req.RequestURI = ""
|
||||||
resp, err := agent.httpClient.Do(req)
|
resp, err := cfg.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -40,20 +40,20 @@ type HealthCheckResponse struct {
|
|||||||
Latency time.Duration `json:"latency"`
|
Latency time.Duration `json:"latency"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
|
func (cfg *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
|
||||||
req := fasthttp.AcquireRequest()
|
req := fasthttp.AcquireRequest()
|
||||||
defer fasthttp.ReleaseRequest(req)
|
defer fasthttp.ReleaseRequest(req)
|
||||||
|
|
||||||
resp := fasthttp.AcquireResponse()
|
resp := fasthttp.AcquireResponse()
|
||||||
defer fasthttp.ReleaseResponse(resp)
|
defer fasthttp.ReleaseResponse(resp)
|
||||||
|
|
||||||
req.SetRequestURI(agentPkg.APIBaseURL + agentPkg.EndpointHealth + "?" + query)
|
req.SetRequestURI(agent.APIBaseURL + agent.EndpointHealth + "?" + query)
|
||||||
req.Header.SetMethod(fasthttp.MethodGet)
|
req.Header.SetMethod(fasthttp.MethodGet)
|
||||||
req.Header.Set("Accept-Encoding", "identity")
|
req.Header.Set("Accept-Encoding", "identity")
|
||||||
req.SetConnectionClose()
|
req.SetConnectionClose()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
err = agent.fasthttpHcClient.DoTimeout(req, resp, timeout)
|
err = cfg.fasthttpHcClient.DoTimeout(req, resp, timeout)
|
||||||
ret.Latency = time.Since(start)
|
ret.Latency = time.Since(start)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ret, err
|
return ret, err
|
||||||
@@ -71,14 +71,14 @@ func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret Heal
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (agent *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
func (cfg *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
||||||
transport := agent.Transport()
|
transport := cfg.Transport()
|
||||||
dialer := websocket.Dialer{
|
dialer := websocket.Dialer{
|
||||||
NetDialContext: transport.DialContext,
|
NetDialContext: transport.DialContext,
|
||||||
NetDialTLSContext: transport.DialTLSContext,
|
NetDialTLSContext: transport.DialTLSContext,
|
||||||
}
|
}
|
||||||
return dialer.DialContext(ctx, agentPkg.APIBaseURL+endpoint, http.Header{
|
return dialer.DialContext(ctx, agent.APIBaseURL+endpoint, http.Header{
|
||||||
"Host": {agentPkg.AgentHost},
|
"Host": {agent.AgentHost},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,9 +86,9 @@ func (agent *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.
|
|||||||
//
|
//
|
||||||
// It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint
|
// It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint
|
||||||
// If the request has a query, it will be added to the proxy request's URL
|
// If the request has a query, it will be added to the proxy request's URL
|
||||||
func (agent *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
|
func (cfg *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
|
||||||
rp := reverseproxy.NewReverseProxy("agent", agentPkg.AgentURL, agent.Transport())
|
rp := reverseproxy.NewReverseProxy("agent", agent.AgentURL, cfg.Transport())
|
||||||
req.URL.Host = agentPkg.AgentHost
|
req.URL.Host = agent.AgentHost
|
||||||
req.URL.Scheme = "https"
|
req.URL.Scheme = "https"
|
||||||
req.URL.Path = endpoint
|
req.URL.Path = endpoint
|
||||||
req.RequestURI = ""
|
req.RequestURI = ""
|
||||||
|
|||||||
@@ -21,23 +21,22 @@ import (
|
|||||||
"github.com/yusing/godoxy/internal/auth"
|
"github.com/yusing/godoxy/internal/auth"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHandler creates a new Gin engine for the API.
|
|
||||||
//
|
|
||||||
// @title GoDoxy API
|
// @title GoDoxy API
|
||||||
// @version 1.0
|
// @version 1.0
|
||||||
// @description GoDoxy API
|
// @description GoDoxy API
|
||||||
// @termsOfService https://github.com/yusing/godoxy/blob/main/LICENSE
|
// @termsOfService https://github.com/yusing/godoxy/blob/main/LICENSE
|
||||||
//
|
|
||||||
// @contact.name Yusing
|
// @contact.name Yusing
|
||||||
// @contact.url https://github.com/yusing/godoxy/issues
|
// @contact.url https://github.com/yusing/godoxy/issues
|
||||||
//
|
|
||||||
// @license.name MIT
|
// @license.name MIT
|
||||||
// @license.url https://github.com/yusing/godoxy/blob/main/LICENSE
|
// @license.url https://github.com/yusing/godoxy/blob/main/LICENSE
|
||||||
//
|
|
||||||
// @BasePath /api/v1
|
// @BasePath /api/v1
|
||||||
//
|
|
||||||
// @externalDocs.description GoDoxy Docs
|
// @externalDocs.description GoDoxy Docs
|
||||||
// @externalDocs.url https://docs.godoxy.dev
|
// @externalDocs.url https://docs.godoxy.dev
|
||||||
func NewHandler(requireAuth bool) *gin.Engine {
|
func NewHandler(requireAuth bool) *gin.Engine {
|
||||||
@@ -77,8 +76,8 @@ func NewHandler(requireAuth bool) *gin.Engine {
|
|||||||
v1.GET("/favicon", apiV1.FavIcon)
|
v1.GET("/favicon", apiV1.FavIcon)
|
||||||
v1.GET("/health", apiV1.Health)
|
v1.GET("/health", apiV1.Health)
|
||||||
v1.GET("/icons", apiV1.Icons)
|
v1.GET("/icons", apiV1.Icons)
|
||||||
|
v1.POST("/reload", apiV1.Reload)
|
||||||
v1.GET("/stats", apiV1.Stats)
|
v1.GET("/stats", apiV1.Stats)
|
||||||
v1.GET("/events", apiV1.Events)
|
|
||||||
|
|
||||||
route := v1.Group("/route")
|
route := v1.Group("/route")
|
||||||
{
|
{
|
||||||
@@ -205,8 +204,9 @@ func ErrorHandler() gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
c.Next()
|
c.Next()
|
||||||
if len(c.Errors) > 0 {
|
if len(c.Errors) > 0 {
|
||||||
|
logger := log.With().Str("uri", c.Request.RequestURI).Logger()
|
||||||
for _, err := range c.Errors {
|
for _, err := range c.Errors {
|
||||||
log.Err(err.Err).Str("uri", c.Request.RequestURI).Msg("Internal error")
|
gperr.LogError("Internal error", err.Err, &logger)
|
||||||
}
|
}
|
||||||
if !c.IsWebsocket() {
|
if !c.IsWebsocket() {
|
||||||
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
|
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# internal/api/v1
|
# API v1 Package
|
||||||
|
|
||||||
Implements the v1 REST API handlers for GoDoxy, exposing endpoints for managing routes, Docker containers, certificates, metrics, and system configuration.
|
Implements the v1 REST API handlers for GoDoxy, exposing endpoints for managing routes, Docker containers, certificates, metrics, and system configuration.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package agentapi
|
package agentapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -14,6 +12,7 @@ import (
|
|||||||
config "github.com/yusing/godoxy/internal/config/types"
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
"github.com/yusing/godoxy/internal/route/provider"
|
"github.com/yusing/godoxy/internal/route/provider"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type VerifyNewAgentRequest struct {
|
type VerifyNewAgentRequest struct {
|
||||||
@@ -37,9 +36,6 @@ type VerifyNewAgentRequest struct {
|
|||||||
// @Failure 500 {object} ErrorResponse
|
// @Failure 500 {object} ErrorResponse
|
||||||
// @Router /agent/verify [post]
|
// @Router /agent/verify [post]
|
||||||
func Verify(c *gin.Context) {
|
func Verify(c *gin.Context) {
|
||||||
// avoid timeout waiting for response headers
|
|
||||||
c.Status(http.StatusContinue)
|
|
||||||
|
|
||||||
var request VerifyNewAgentRequest
|
var request VerifyNewAgentRequest
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
@@ -64,7 +60,7 @@ func Verify(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nRoutesAdded, err := verifyNewAgent(c.Request.Context(), request.Host, ca, client, request.ContainerRuntime)
|
nRoutesAdded, err := verifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
return
|
return
|
||||||
@@ -84,9 +80,9 @@ func Verify(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
|
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
|
||||||
}
|
}
|
||||||
|
|
||||||
var errAgentAlreadyExists = errors.New("agent already exists")
|
var errAgentAlreadyExists = gperr.New("agent already exists")
|
||||||
|
|
||||||
func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, error) {
|
func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
|
||||||
var agentCfg agent.AgentConfig
|
var agentCfg agent.AgentConfig
|
||||||
agentCfg.Addr = host
|
agentCfg.Addr = host
|
||||||
agentCfg.Runtime = containerRuntime
|
agentCfg.Runtime = containerRuntime
|
||||||
@@ -103,14 +99,14 @@ func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client a
|
|||||||
return 0, errAgentAlreadyExists
|
return 0, errAgentAlreadyExists
|
||||||
}
|
}
|
||||||
|
|
||||||
err := agentCfg.InitWithCerts(ctx, ca.Cert, client.Cert, client.Key)
|
err := agentCfg.InitWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to initialize agent config: %w", err)
|
return 0, gperr.Wrap(err, "failed to initialize agent config")
|
||||||
}
|
}
|
||||||
|
|
||||||
provider := provider.NewAgentProvider(&agentCfg)
|
provider := provider.NewAgentProvider(&agentCfg)
|
||||||
if _, loaded := cfgState.LoadOrStoreProvider(provider.String(), provider); loaded {
|
if _, loaded := cfgState.LoadOrStoreProvider(provider.String(), provider); loaded {
|
||||||
return 0, fmt.Errorf("provider %s already exists", provider.String())
|
return 0, gperr.Errorf("provider %s already exists", provider.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// agent must be added before loading routes
|
// agent must be added before loading routes
|
||||||
@@ -122,7 +118,7 @@ func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client a
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
cfgState.DeleteProvider(provider.String())
|
cfgState.DeleteProvider(provider.String())
|
||||||
agentpool.Remove(&agentCfg)
|
agentpool.Remove(&agentCfg)
|
||||||
return 0, fmt.Errorf("failed to load routes: %w", err)
|
return 0, gperr.Wrap(err, "failed to load routes")
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider.NumRoutes(), nil
|
return provider.NumRoutes(), nil
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
autocertctx "github.com/yusing/godoxy/internal/autocert/types"
|
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ import (
|
|||||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
// @Router /cert/info [get]
|
// @Router /cert/info [get]
|
||||||
func Info(c *gin.Context) {
|
func Info(c *gin.Context) {
|
||||||
provider := autocertctx.FromCtx(c.Request.Context())
|
provider := autocert.ActiveProvider.Load()
|
||||||
if provider == nil {
|
if provider == nil {
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
autocertctx "github.com/yusing/godoxy/internal/autocert/types"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
@@ -23,8 +23,8 @@ import (
|
|||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /cert/renew [get]
|
// @Router /cert/renew [get]
|
||||||
func Renew(c *gin.Context) {
|
func Renew(c *gin.Context) {
|
||||||
provider := autocertctx.FromCtx(c.Request.Context())
|
autocert := autocert.ActiveProvider.Load()
|
||||||
if provider == nil {
|
if autocert == nil {
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ func Renew(c *gin.Context) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// renewal happens in background
|
// renewal happens in background
|
||||||
ok := provider.ForceExpiryAll()
|
ok := autocert.ForceExpiryAll()
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Error().Msg("cert renewal already in progress")
|
log.Error().Msg("cert renewal already in progress")
|
||||||
time.Sleep(1 * time.Second) // wait for the log above to be sent
|
time.Sleep(1 * time.Second) // wait for the log above to be sent
|
||||||
@@ -67,5 +67,5 @@ func Renew(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
log.Info().Msg("cert force renewal requested")
|
log.Info().Msg("cert force renewal requested")
|
||||||
|
|
||||||
provider.WaitRenewalDone(manager.Context())
|
autocert.WaitRenewalDone(manager.Context())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/moby/moby/api/types/container"
|
"github.com/moby/moby/api/types/container"
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
|
||||||
_ "github.com/yusing/goutils/apitypes"
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
@@ -37,18 +36,18 @@ func Containers(c *gin.Context) {
|
|||||||
serveHTTP[Container](c, GetContainers)
|
serveHTTP[Container](c, GetContainers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, error) {
|
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {
|
||||||
errs := gperr.NewBuilder("failed to get containers")
|
errs := gperr.NewBuilder("failed to get containers")
|
||||||
containers := make([]Container, 0)
|
containers := make([]Container, 0)
|
||||||
for name, dockerClient := range dockerClients {
|
for server, dockerClient := range dockerClients {
|
||||||
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
|
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.AddSubject(err, name)
|
errs.Add(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, cont := range conts.Items {
|
for _, cont := range conts.Items {
|
||||||
containers = append(containers, Container{
|
containers = append(containers, Container{
|
||||||
Server: name,
|
Server: server,
|
||||||
Name: cont.Names[0],
|
Name: cont.Names[0],
|
||||||
ID: cont.ID,
|
ID: cont.ID,
|
||||||
Image: cont.Image,
|
Image: cont.Image,
|
||||||
@@ -60,10 +59,11 @@ func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Containe
|
|||||||
return containers[i].Name < containers[j].Name
|
return containers[i].Name < containers[j].Name
|
||||||
})
|
})
|
||||||
if err := errs.Error(); err != nil {
|
if err := errs.Error(); err != nil {
|
||||||
if len(containers) > 0 {
|
gperr.LogError("failed to get containers", err)
|
||||||
log.Err(err).Msg("failed to get containers from some servers")
|
if len(containers) == 0 {
|
||||||
return containers, nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return containers, nil
|
||||||
}
|
}
|
||||||
return containers, errs.Error()
|
return containers, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func Info(c *gin.Context) {
|
|||||||
serveHTTP[dockerInfo](c, GetDockerInfo)
|
serveHTTP[dockerInfo](c, GetDockerInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, error) {
|
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, gperr.Error) {
|
||||||
errs := gperr.NewBuilder("failed to get docker info")
|
errs := gperr.NewBuilder("failed to get docker info")
|
||||||
dockerInfos := make([]dockerInfo, len(dockerClients))
|
dockerInfos := make([]dockerInfo, len(dockerClients))
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerIn
|
|||||||
for name, dockerClient := range dockerClients {
|
for name, dockerClient := range dockerClients {
|
||||||
info, err := dockerClient.Info(ctx, client.InfoOptions{})
|
info, err := dockerClient.Info(ctx, client.InfoOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.AddSubject(err, name)
|
errs.Add(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
info.Info.Name = name
|
info.Info.Name = name
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/moby/moby/api/types/container"
|
"github.com/moby/moby/api/types/container"
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
@@ -44,7 +44,7 @@ func Stats(c *gin.Context) {
|
|||||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
||||||
if !ok {
|
if !ok {
|
||||||
var route types.Route
|
var route types.Route
|
||||||
route, ok = entrypoint.FromCtx(c.Request.Context()).GetRoute(id)
|
route, ok = routes.GetIncludeExcluded(id)
|
||||||
if ok {
|
if ok {
|
||||||
cont := route.ContainerInfo()
|
cont := route.ContainerInfo()
|
||||||
if cont == nil {
|
if cont == nil {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
@@ -38,7 +39,7 @@ func handleResult[V any, T ResultType[V]](c *gin.Context, errs error, result T)
|
|||||||
c.JSON(http.StatusOK, result)
|
c.JSON(http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, error)) {
|
func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
|
||||||
dockerClients := docker.Clients()
|
dockerClients := docker.Clients()
|
||||||
defer closeAllClients(dockerClients)
|
defer closeAllClients(dockerClients)
|
||||||
|
|
||||||
|
|||||||
@@ -837,45 +837,6 @@
|
|||||||
"operationId": "stop"
|
"operationId": "stop"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/events": {
|
|
||||||
"get": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Get events history",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/Event"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden: unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error: internal error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-id": "events",
|
|
||||||
"operationId": "events"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/favicon": {
|
"/favicon": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get favicon",
|
"description": "Get favicon",
|
||||||
@@ -952,6 +913,7 @@
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
|
"application/json",
|
||||||
"application/godoxy+yaml"
|
"application/godoxy+yaml"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -1257,12 +1219,6 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/ErrorResponse"
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-id": "categories",
|
"x-id": "categories",
|
||||||
@@ -1381,12 +1337,6 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/ErrorResponse"
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-id": "items",
|
"x-id": "items",
|
||||||
@@ -2870,6 +2820,43 @@
|
|||||||
"operationId": "tail"
|
"operationId": "tail"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/reload": {
|
||||||
|
"post": {
|
||||||
|
"description": "Reload config",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"v1"
|
||||||
|
],
|
||||||
|
"summary": "Reload config",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/SuccessResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-id": "reload",
|
||||||
|
"operationId": "reload"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/route/by_provider": {
|
"/route/by_provider": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List routes by provider",
|
"description": "List routes by provider",
|
||||||
@@ -2946,8 +2933,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-id": "list",
|
"x-id": "routes",
|
||||||
"operationId": "list"
|
"operationId": "routes"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/route/playground": {
|
"/route/playground": {
|
||||||
@@ -3823,42 +3810,6 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"Event": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"action": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"category": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"level": {
|
|
||||||
"$ref": "#/definitions/events.Level",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"timestamp": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"uuid": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"FileType": {
|
"FileType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -4028,6 +3979,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"latency": {
|
"latency": {
|
||||||
|
"description": "latency in microseconds",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
@@ -4046,6 +3998,7 @@
|
|||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"uptime": {
|
"uptime": {
|
||||||
|
"description": "uptime in milliseconds",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
@@ -4121,7 +4074,7 @@
|
|||||||
"HealthMap": {
|
"HealthMap": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/definitions/HealthInfoWithoutDetail"
|
"$ref": "#/definitions/HealthStatusString"
|
||||||
},
|
},
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
@@ -4844,19 +4797,16 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"days": {
|
"days": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 0,
|
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"keep_size": {
|
"keep_size": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 0,
|
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"last": {
|
"last": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 0,
|
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
}
|
}
|
||||||
@@ -5093,6 +5043,11 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
|
"isResponseRule": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
@@ -5104,7 +5059,6 @@
|
|||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"validationError": {
|
"validationError": {
|
||||||
"description": "we need the structured error, not the plain string",
|
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
}
|
}
|
||||||
@@ -5140,7 +5094,6 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"executionError": {
|
"executionError": {
|
||||||
"description": "we need the structured error, not the plain string",
|
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
@@ -5376,6 +5329,7 @@
|
|||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"bind": {
|
"bind": {
|
||||||
|
"description": "for TCP and UDP routes, bind address to listen on",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-nullable": true
|
"x-nullable": true
|
||||||
},
|
},
|
||||||
@@ -6642,13 +6596,11 @@
|
|||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"fstype": {
|
"fstype": {
|
||||||
"description": "interned",
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"path": {
|
"path": {
|
||||||
"description": "interned",
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
@@ -6739,23 +6691,6 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"events.Level": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"debug",
|
|
||||||
"info",
|
|
||||||
"warn",
|
|
||||||
"error"
|
|
||||||
],
|
|
||||||
"x-enum-varnames": [
|
|
||||||
"LevelDebug",
|
|
||||||
"LevelInfo",
|
|
||||||
"LevelWarn",
|
|
||||||
"LevelError"
|
|
||||||
],
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"icons.Source": {
|
"icons.Source": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -6995,7 +6930,6 @@
|
|||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"description": "interned",
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
|
|||||||
@@ -295,20 +295,6 @@ definitions:
|
|||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
Event:
|
|
||||||
properties:
|
|
||||||
action:
|
|
||||||
type: string
|
|
||||||
category:
|
|
||||||
type: string
|
|
||||||
data: {}
|
|
||||||
level:
|
|
||||||
$ref: '#/definitions/events.Level'
|
|
||||||
timestamp:
|
|
||||||
type: string
|
|
||||||
uuid:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
FileType:
|
FileType:
|
||||||
enum:
|
enum:
|
||||||
- config
|
- config
|
||||||
@@ -389,6 +375,7 @@ definitions:
|
|||||||
HealthInfoWithoutDetail:
|
HealthInfoWithoutDetail:
|
||||||
properties:
|
properties:
|
||||||
latency:
|
latency:
|
||||||
|
description: latency in microseconds
|
||||||
type: number
|
type: number
|
||||||
status:
|
status:
|
||||||
enum:
|
enum:
|
||||||
@@ -400,6 +387,7 @@ definitions:
|
|||||||
- unknown
|
- unknown
|
||||||
type: string
|
type: string
|
||||||
uptime:
|
uptime:
|
||||||
|
description: uptime in milliseconds
|
||||||
type: number
|
type: number
|
||||||
type: object
|
type: object
|
||||||
HealthJSON:
|
HealthJSON:
|
||||||
@@ -433,7 +421,7 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
HealthMap:
|
HealthMap:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
$ref: '#/definitions/HealthInfoWithoutDetail'
|
$ref: '#/definitions/HealthStatusString'
|
||||||
type: object
|
type: object
|
||||||
HealthStatusString:
|
HealthStatusString:
|
||||||
enum:
|
enum:
|
||||||
@@ -767,13 +755,10 @@ definitions:
|
|||||||
LogRetention:
|
LogRetention:
|
||||||
properties:
|
properties:
|
||||||
days:
|
days:
|
||||||
minimum: 0
|
|
||||||
type: integer
|
type: integer
|
||||||
keep_size:
|
keep_size:
|
||||||
minimum: 0
|
|
||||||
type: integer
|
type: integer
|
||||||
last:
|
last:
|
||||||
minimum: 0
|
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
MetricsPeriod:
|
MetricsPeriod:
|
||||||
@@ -891,12 +876,13 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
do:
|
do:
|
||||||
type: string
|
type: string
|
||||||
|
isResponseRule:
|
||||||
|
type: boolean
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
"on":
|
"on":
|
||||||
type: string
|
type: string
|
||||||
validationError:
|
validationError: {}
|
||||||
description: we need the structured error, not the plain string
|
|
||||||
type: object
|
type: object
|
||||||
PlaygroundRequest:
|
PlaygroundRequest:
|
||||||
properties:
|
properties:
|
||||||
@@ -913,8 +899,7 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
PlaygroundResponse:
|
PlaygroundResponse:
|
||||||
properties:
|
properties:
|
||||||
executionError:
|
executionError: {}
|
||||||
description: we need the structured error, not the plain string
|
|
||||||
finalRequest:
|
finalRequest:
|
||||||
$ref: '#/definitions/FinalRequest'
|
$ref: '#/definitions/FinalRequest'
|
||||||
finalResponse:
|
finalResponse:
|
||||||
@@ -1022,6 +1007,7 @@ definitions:
|
|||||||
alias:
|
alias:
|
||||||
type: string
|
type: string
|
||||||
bind:
|
bind:
|
||||||
|
description: for TCP and UDP routes, bind address to listen on
|
||||||
type: string
|
type: string
|
||||||
x-nullable: true
|
x-nullable: true
|
||||||
container:
|
container:
|
||||||
@@ -1691,10 +1677,8 @@ definitions:
|
|||||||
free:
|
free:
|
||||||
type: integer
|
type: integer
|
||||||
fstype:
|
fstype:
|
||||||
description: interned
|
|
||||||
type: string
|
type: string
|
||||||
path:
|
path:
|
||||||
description: interned
|
|
||||||
type: string
|
type: string
|
||||||
total:
|
total:
|
||||||
type: number
|
type: number
|
||||||
@@ -1762,18 +1746,6 @@ definitions:
|
|||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
type: object
|
type: object
|
||||||
events.Level:
|
|
||||||
enum:
|
|
||||||
- debug
|
|
||||||
- info
|
|
||||||
- warn
|
|
||||||
- error
|
|
||||||
type: string
|
|
||||||
x-enum-varnames:
|
|
||||||
- LevelDebug
|
|
||||||
- LevelInfo
|
|
||||||
- LevelWarn
|
|
||||||
- LevelError
|
|
||||||
icons.Source:
|
icons.Source:
|
||||||
enum:
|
enum:
|
||||||
- https://
|
- https://
|
||||||
@@ -1889,7 +1861,6 @@ definitions:
|
|||||||
high:
|
high:
|
||||||
type: number
|
type: number
|
||||||
name:
|
name:
|
||||||
description: interned
|
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
type: number
|
type: number
|
||||||
@@ -2481,31 +2452,6 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- docker
|
- docker
|
||||||
x-id: stop
|
x-id: stop
|
||||||
/events:
|
|
||||||
get:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/Event'
|
|
||||||
type: array
|
|
||||||
"403":
|
|
||||||
description: 'Forbidden: unauthorized'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ErrorResponse'
|
|
||||||
"500":
|
|
||||||
description: 'Internal Server Error: internal error'
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ErrorResponse'
|
|
||||||
summary: Get events history
|
|
||||||
tags:
|
|
||||||
- v1
|
|
||||||
x-id: events
|
|
||||||
/favicon:
|
/favicon:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -2576,6 +2522,7 @@ paths:
|
|||||||
- FileTypeProvider
|
- FileTypeProvider
|
||||||
- FileTypeMiddleware
|
- FileTypeMiddleware
|
||||||
produces:
|
produces:
|
||||||
|
- application/json
|
||||||
- application/godoxy+yaml
|
- application/godoxy+yaml
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
@@ -2760,10 +2707,6 @@ paths:
|
|||||||
description: Forbidden
|
description: Forbidden
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ErrorResponse'
|
$ref: '#/definitions/ErrorResponse'
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ErrorResponse'
|
|
||||||
summary: List homepage categories
|
summary: List homepage categories
|
||||||
tags:
|
tags:
|
||||||
- homepage
|
- homepage
|
||||||
@@ -2841,10 +2784,6 @@ paths:
|
|||||||
description: Forbidden
|
description: Forbidden
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ErrorResponse'
|
$ref: '#/definitions/ErrorResponse'
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ErrorResponse'
|
|
||||||
summary: Homepage items
|
summary: Homepage items
|
||||||
tags:
|
tags:
|
||||||
- homepage
|
- homepage
|
||||||
@@ -3851,6 +3790,30 @@ paths:
|
|||||||
- proxmox
|
- proxmox
|
||||||
- websocket
|
- websocket
|
||||||
x-id: tail
|
x-id: tail
|
||||||
|
/reload:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Reload config
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/SuccessResponse'
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
summary: Reload config
|
||||||
|
tags:
|
||||||
|
- v1
|
||||||
|
x-id: reload
|
||||||
/route/{which}:
|
/route/{which}:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -3936,7 +3899,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- route
|
- route
|
||||||
- websocket
|
- websocket
|
||||||
x-id: list
|
x-id: routes
|
||||||
/route/playground:
|
/route/playground:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
|
||||||
"github.com/yusing/goutils/events"
|
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
|
||||||
"github.com/yusing/goutils/http/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @x-id "events"
|
|
||||||
// @BasePath /api/v1
|
|
||||||
// @Summary Get events history
|
|
||||||
// @Tags v1
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {array} events.Event
|
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse "Forbidden: unauthorized"
|
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal Server Error: internal error"
|
|
||||||
// @Router /events [get]
|
|
||||||
func Events(c *gin.Context) {
|
|
||||||
if !httpheaders.IsWebsocket(c.Request.Header) {
|
|
||||||
c.JSON(http.StatusOK, events.Global.Get())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
|
||||||
if err != nil {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer manager.Close()
|
|
||||||
|
|
||||||
writer := manager.NewWriter(websocket.TextMessage)
|
|
||||||
err = events.Global.ListenJSON(c.Request.Context(), writer)
|
|
||||||
if err != nil && !errors.Is(err, context.Canceled) {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to listen to events"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
|
||||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||||
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
|
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
|
||||||
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
|
||||||
_ "unsafe"
|
_ "unsafe"
|
||||||
@@ -73,11 +73,7 @@ func FavIcon(c *gin.Context) {
|
|||||||
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
|
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
|
||||||
func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) {
|
func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) {
|
||||||
// try with route.Icon
|
// try with route.Icon
|
||||||
ep := entrypoint.FromCtx(ctx)
|
r, ok := routes.HTTP.Get(alias)
|
||||||
if ep == nil { // impossible, but just in case
|
|
||||||
return iconfetch.FetchResultWithErrorf(http.StatusInternalServerError, "entrypoint not initialized")
|
|
||||||
}
|
|
||||||
r, ok := ep.HTTPRoutes().Get(alias)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type GetFileContentRequest struct {
|
|||||||
// @Description Get file content
|
// @Description Get file content
|
||||||
// @Tags file
|
// @Tags file
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce application/godoxy+yaml
|
// @Produce json,application/godoxy+yaml
|
||||||
// @Param query query GetFileContentRequest true "Request"
|
// @Param query query GetFileContentRequest true "Request"
|
||||||
// @Success 200 {string} application/godoxy+yaml "File content"
|
// @Success 200 {string} application/godoxy+yaml "File content"
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse
|
// @Failure 400 {object} apitypes.ErrorResponse
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func Validate(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, apitypes.Success("file validated"))
|
c.JSON(http.StatusOK, apitypes.Success("file validated"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateFile(fileType FileType, content []byte) error {
|
func validateFile(fileType FileType, content []byte) gperr.Error {
|
||||||
switch fileType {
|
switch fileType {
|
||||||
case FileTypeConfig:
|
case FileTypeConfig:
|
||||||
return config.Validate(content)
|
return config.Validate(content)
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
|
||||||
"github.com/yusing/goutils/apitypes"
|
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
|
||||||
|
|
||||||
type HealthMap = map[string]types.HealthInfoWithoutDetail // @name HealthMap
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
|
)
|
||||||
|
|
||||||
// @x-id "health"
|
// @x-id "health"
|
||||||
// @BasePath /api/v1
|
// @BasePath /api/v1
|
||||||
@@ -21,21 +19,16 @@ type HealthMap = map[string]types.HealthInfoWithoutDetail // @name HealthMap
|
|||||||
// @Tags v1,websocket
|
// @Tags v1,websocket
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} HealthMap "Health info by route name"
|
// @Success 200 {object} routes.HealthMap "Health info by route name"
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /health [get]
|
// @Router /health [get]
|
||||||
func Health(c *gin.Context) {
|
func Health(c *gin.Context) {
|
||||||
ep := entrypoint.FromCtx(c.Request.Context())
|
|
||||||
if ep == nil { // impossible, but just in case
|
|
||||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||||
websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) {
|
websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) {
|
||||||
return ep.GetHealthInfoWithoutDetail(), nil
|
return routes.GetHealthInfoSimple(), nil
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, ep.GetHealthInfoWithoutDetail())
|
c.JSON(http.StatusOK, routes.GetHealthInfoSimple())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
|
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "categories"
|
// @x-id "categories"
|
||||||
@@ -19,23 +19,17 @@ import (
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} string
|
// @Success 200 {array} string
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /homepage/categories [get]
|
// @Router /homepage/categories [get]
|
||||||
func Categories(c *gin.Context) {
|
func Categories(c *gin.Context) {
|
||||||
ep := entrypoint.FromCtx(c.Request.Context())
|
c.JSON(http.StatusOK, HomepageCategories())
|
||||||
if ep == nil { // impossible, but just in case
|
|
||||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, HomepageCategories(ep))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func HomepageCategories(ep entrypoint.Entrypoint) []string {
|
func HomepageCategories() []string {
|
||||||
check := make(map[string]struct{})
|
check := make(map[string]struct{})
|
||||||
categories := make([]string, 0)
|
categories := make([]string, 0)
|
||||||
categories = append(categories, homepage.CategoryAll)
|
categories = append(categories, homepage.CategoryAll)
|
||||||
categories = append(categories, homepage.CategoryFavorites)
|
categories = append(categories, homepage.CategoryFavorites)
|
||||||
for _, r := range ep.HTTPRoutes().Iter {
|
for _, r := range routes.HTTP.Iter {
|
||||||
item := r.HomepageItem()
|
item := r.HomepageItem()
|
||||||
if item.Category == "" {
|
if item.Category == "" {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
@@ -36,7 +36,6 @@ type HomepageItemsRequest struct {
|
|||||||
// @Success 200 {object} homepage.Homepage
|
// @Success 200 {object} homepage.Homepage
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse
|
// @Failure 400 {object} apitypes.ErrorResponse
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /homepage/items [get]
|
// @Router /homepage/items [get]
|
||||||
func Items(c *gin.Context) {
|
func Items(c *gin.Context) {
|
||||||
var request HomepageItemsRequest
|
var request HomepageItemsRequest
|
||||||
@@ -54,35 +53,29 @@ func Items(c *gin.Context) {
|
|||||||
hostname = host
|
hostname = host
|
||||||
}
|
}
|
||||||
|
|
||||||
ep := entrypoint.FromCtx(c.Request.Context())
|
|
||||||
if ep == nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not found in context", nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||||
websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) {
|
websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) {
|
||||||
return HomepageItems(ep, proto, hostname, &request), nil
|
return HomepageItems(proto, hostname, &request), nil
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, HomepageItems(ep, proto, hostname, &request))
|
c.JSON(http.StatusOK, HomepageItems(proto, hostname, &request))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HomepageItems(ep entrypoint.Entrypoint, proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
|
func HomepageItems(proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
|
||||||
switch proto {
|
switch proto {
|
||||||
case "http", "https":
|
case "http", "https":
|
||||||
default:
|
default:
|
||||||
proto = "http"
|
proto = "http"
|
||||||
}
|
}
|
||||||
|
|
||||||
hp := homepage.NewHomepageMap(ep.HTTPRoutes().Size())
|
hp := homepage.NewHomepageMap(routes.HTTP.Size())
|
||||||
|
|
||||||
if strings.Count(hostname, ".") > 1 {
|
if strings.Count(hostname, ".") > 1 {
|
||||||
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
|
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range ep.HTTPRoutes().Iter {
|
for _, r := range routes.HTTP.Iter {
|
||||||
if request.Provider != "" && r.ProviderName() != request.Provider {
|
if request.Provider != "" && r.ProviderName() != request.Provider {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/cenkalti/backoff/v5"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
@@ -37,11 +35,6 @@ type bytesFromPool struct {
|
|||||||
release func([]byte)
|
release func([]byte)
|
||||||
}
|
}
|
||||||
|
|
||||||
type systemInfoData struct {
|
|
||||||
agentName string
|
|
||||||
systemInfo any
|
|
||||||
}
|
|
||||||
|
|
||||||
// @x-id "all_system_info"
|
// @x-id "all_system_info"
|
||||||
// @BasePath /api/v1
|
// @BasePath /api/v1
|
||||||
// @Summary Get system info
|
// @Summary Get system info
|
||||||
@@ -79,19 +72,91 @@ func AllSystemInfo(c *gin.Context) {
|
|||||||
defer manager.Close()
|
defer manager.Close()
|
||||||
|
|
||||||
query := c.Request.URL.Query()
|
query := c.Request.URL.Query()
|
||||||
queryEncoded := query.Encode()
|
queryEncoded := c.Request.URL.Query().Encode()
|
||||||
|
|
||||||
|
type SystemInfoData struct {
|
||||||
|
AgentName string
|
||||||
|
SystemInfo any
|
||||||
|
}
|
||||||
|
|
||||||
// leave 5 extra slots for buffering in case new agents are added.
|
// leave 5 extra slots for buffering in case new agents are added.
|
||||||
dataCh := make(chan systemInfoData, 1+agentpool.Num()+5)
|
dataCh := make(chan SystemInfoData, 1+agentpool.Num()+5)
|
||||||
|
defer close(dataCh)
|
||||||
|
|
||||||
ticker := time.NewTicker(req.Interval)
|
ticker := time.NewTicker(req.Interval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
go streamSystemInfo(manager, dataCh)
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-manager.Done():
|
||||||
|
return
|
||||||
|
case data := <-dataCh:
|
||||||
|
err := marshalSystemInfo(manager, data.AgentName, data.SystemInfo)
|
||||||
|
if err != nil {
|
||||||
|
manager.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// processing function for one round.
|
||||||
|
doRound := func() (bool, error) {
|
||||||
|
var numErrs atomic.Int32
|
||||||
|
|
||||||
|
totalAgents := int32(1) // myself
|
||||||
|
|
||||||
|
var errs gperr.Group
|
||||||
|
// get system info for me and all agents in parallel.
|
||||||
|
errs.Go(func() error {
|
||||||
|
data, err := systeminfo.Poller.GetRespData(req.Period, query)
|
||||||
|
if err != nil {
|
||||||
|
numErrs.Add(1)
|
||||||
|
return gperr.PrependSubject("Main server", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-manager.Done():
|
||||||
|
return nil
|
||||||
|
case dataCh <- SystemInfoData{
|
||||||
|
AgentName: "GoDoxy",
|
||||||
|
SystemInfo: data,
|
||||||
|
}:
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, a := range agentpool.Iter() {
|
||||||
|
totalAgents++
|
||||||
|
|
||||||
|
errs.Go(func() error {
|
||||||
|
data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded)
|
||||||
|
if err != nil {
|
||||||
|
numErrs.Add(1)
|
||||||
|
return gperr.PrependSubject("Agent "+a.Name, err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-manager.Done():
|
||||||
|
return nil
|
||||||
|
case dataCh <- SystemInfoData{
|
||||||
|
AgentName: a.Name,
|
||||||
|
SystemInfo: data,
|
||||||
|
}:
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err := errs.Wait().Error()
|
||||||
|
return numErrs.Load() == totalAgents, err
|
||||||
|
}
|
||||||
|
|
||||||
// write system info immediately once.
|
// write system info immediately once.
|
||||||
if hasSuccess, err := collectSystemInfoRound(manager, req, query, queryEncoded, dataCh); handleRoundResult(c, hasSuccess, err, false) {
|
if shouldContinue, err := doRound(); err != nil {
|
||||||
return
|
if !shouldContinue {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// then continue on the ticker.
|
// then continue on the ticker.
|
||||||
@@ -100,95 +165,17 @@ func AllSystemInfo(c *gin.Context) {
|
|||||||
case <-manager.Done():
|
case <-manager.Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if hasSuccess, err := collectSystemInfoRound(manager, req, query, queryEncoded, dataCh); handleRoundResult(c, hasSuccess, err, true) {
|
if shouldContinue, err := doRound(); err != nil {
|
||||||
return
|
if !shouldContinue {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gperr.LogWarn("failed to get some system info", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamSystemInfo(manager *websocket.Manager, dataCh <-chan systemInfoData) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-manager.Done():
|
|
||||||
return
|
|
||||||
case data := <-dataCh:
|
|
||||||
err := marshalSystemInfo(manager, data.agentName, data.systemInfo)
|
|
||||||
if err != nil {
|
|
||||||
manager.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func queueSystemInfo(manager *websocket.Manager, dataCh chan<- systemInfoData, data systemInfoData) {
|
|
||||||
select {
|
|
||||||
case <-manager.Done():
|
|
||||||
case dataCh <- data:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectSystemInfoRound(
|
|
||||||
manager *websocket.Manager,
|
|
||||||
req AllSystemInfoRequest,
|
|
||||||
query url.Values,
|
|
||||||
queryEncoded string,
|
|
||||||
dataCh chan<- systemInfoData,
|
|
||||||
) (hasSuccess bool, err error) {
|
|
||||||
var numErrs atomic.Int32
|
|
||||||
totalAgents := int32(1) // myself
|
|
||||||
|
|
||||||
var errs gperr.Group
|
|
||||||
// get system info for me and all agents in parallel.
|
|
||||||
errs.Go(func() error {
|
|
||||||
data, err := systeminfo.Poller.GetRespData(req.Period, query)
|
|
||||||
if err != nil {
|
|
||||||
numErrs.Add(1)
|
|
||||||
return gperr.PrependSubject(err, "Main server")
|
|
||||||
}
|
|
||||||
queueSystemInfo(manager, dataCh, systemInfoData{
|
|
||||||
agentName: "GoDoxy",
|
|
||||||
systemInfo: data,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, a := range agentpool.Iter() {
|
|
||||||
totalAgents++
|
|
||||||
|
|
||||||
errs.Go(func() error {
|
|
||||||
data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded)
|
|
||||||
if err != nil {
|
|
||||||
numErrs.Add(1)
|
|
||||||
return gperr.PrependSubject(err, "Agent "+a.Name)
|
|
||||||
}
|
|
||||||
queueSystemInfo(manager, dataCh, systemInfoData{
|
|
||||||
agentName: a.Name,
|
|
||||||
systemInfo: data,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errs.Wait().Error()
|
|
||||||
return numErrs.Load() < totalAgents, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRoundResult(c *gin.Context, hasSuccess bool, err error, logPartial bool) (stop bool) {
|
|
||||||
if err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !hasSuccess {
|
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if logPartial {
|
|
||||||
log.Warn().Err(err).Msg("failed to get some system info")
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAgentSystemInfo(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) {
|
func getAgentSystemInfo(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -210,26 +197,35 @@ func getAgentSystemInfo(ctx context.Context, a *agentpool.Agent, query string) (
|
|||||||
|
|
||||||
func getAgentSystemInfoWithRetry(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) {
|
func getAgentSystemInfoWithRetry(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) {
|
||||||
const maxRetries = 3
|
const maxRetries = 3
|
||||||
const retryDelay = 5 * time.Second
|
var lastErr error
|
||||||
var attempt int
|
|
||||||
data, err := backoff.Retry(ctx, func() (bytesFromPool, error) {
|
for attempt := range maxRetries {
|
||||||
attempt++
|
// Apply backoff delay for retries (not for first attempt)
|
||||||
|
if attempt > 0 {
|
||||||
|
delay := max((1<<attempt)*time.Second, 5*time.Second)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return bytesFromPool{}, ctx.Err()
|
||||||
|
case <-time.After(delay):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data, err := getAgentSystemInfo(ctx, a, query)
|
data, err := getAgentSystemInfo(ctx, a, query)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Err(err).Str("agent", a.Name).Int("attempt", attempt).Msg("Agent request attempt failed")
|
lastErr = err
|
||||||
return bytesFromPool{}, err
|
|
||||||
},
|
log.Debug().Str("agent", a.Name).Int("attempt", attempt+1).Str("error", err.Error()).Msg("Agent request attempt failed")
|
||||||
backoff.WithBackOff(backoff.NewConstantBackOff(retryDelay)),
|
|
||||||
backoff.WithMaxTries(maxRetries),
|
// Don't retry on context cancellation
|
||||||
)
|
if ctx.Err() != nil {
|
||||||
if err != nil {
|
return bytesFromPool{}, ctx.Err()
|
||||||
return bytesFromPool{}, err
|
}
|
||||||
}
|
}
|
||||||
return data, nil
|
|
||||||
|
return bytesFromPool{}, lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any) error {
|
func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any) error {
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ package proxmoxapi
|
|||||||
|
|
||||||
type ActionRequest struct {
|
type ActionRequest struct {
|
||||||
Node string `uri:"node" binding:"required"`
|
Node string `uri:"node" binding:"required"`
|
||||||
VMID uint64 `uri:"vmid" binding:"required"`
|
VMID int `uri:"vmid" binding:"required"`
|
||||||
} // @name ProxmoxVMActionRequest
|
} // @name ProxmoxVMActionRequest
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/internal/proxmox"
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
"github.com/yusing/goutils/apitypes"
|
"github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,10 +15,10 @@ import (
|
|||||||
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl/pve/127?service=pveproxy&service=pvedaemon&limit=10
|
// e.g. ws://localhost:8889/api/v1/proxmox/journalctl/pve/127?service=pveproxy&service=pvedaemon&limit=10
|
||||||
|
|
||||||
type JournalctlRequest struct {
|
type JournalctlRequest struct {
|
||||||
Node string `form:"node" uri:"node" binding:"required"` // Node name
|
Node string `form:"node" uri:"node" binding:"required"` // Node name
|
||||||
VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl)
|
||||||
Services []string `form:"service" uri:"service"` // Service names
|
Services []string `form:"service" uri:"service"` // Service names
|
||||||
Limit *int `form:"limit" uri:"limit" default:"100" binding:"omitempty,min=1,max=1000"` // Limit output lines (1-1000)
|
Limit *int `form:"limit" uri:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000)
|
||||||
} // @name ProxmoxJournalctlRequest
|
} // @name ProxmoxJournalctlRequest
|
||||||
|
|
||||||
// @x-id "journalctl"
|
// @x-id "journalctl"
|
||||||
@@ -41,11 +40,6 @@ type JournalctlRequest struct {
|
|||||||
// @Router /proxmox/journalctl/{node}/{vmid} [get]
|
// @Router /proxmox/journalctl/{node}/{vmid} [get]
|
||||||
// @Router /proxmox/journalctl/{node}/{vmid}/{service} [get]
|
// @Router /proxmox/journalctl/{node}/{vmid}/{service} [get]
|
||||||
func Journalctl(c *gin.Context) {
|
func Journalctl(c *gin.Context) {
|
||||||
if !httpheaders.IsWebsocket(c.Request.Header) {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("websocket required"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var request JournalctlRequest
|
var request JournalctlRequest
|
||||||
uriErr := c.ShouldBindUri(&request)
|
uriErr := c.ShouldBindUri(&request)
|
||||||
queryErr := c.ShouldBindQuery(&request)
|
queryErr := c.ShouldBindQuery(&request)
|
||||||
@@ -54,10 +48,6 @@ func Journalctl(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.Limit == nil {
|
|
||||||
request.Limit = new(100)
|
|
||||||
}
|
|
||||||
|
|
||||||
node, ok := proxmox.Nodes.Get(request.Node)
|
node, ok := proxmox.Nodes.Get(request.Node)
|
||||||
if !ok {
|
if !ok {
|
||||||
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
c.JSON(http.StatusNotFound, apitypes.Error("node not found"))
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import (
|
|||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StatsRequest ActionRequest
|
type StatsRequest struct {
|
||||||
|
Node string `uri:"node" binding:"required"`
|
||||||
|
VMID int `uri:"vmid" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
// @x-id "nodeStats"
|
// @x-id "nodeStats"
|
||||||
// @BasePath /api/v1
|
// @BasePath /api/v1
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/internal/proxmox"
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
"github.com/yusing/goutils/apitypes"
|
"github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,11 +34,6 @@ type TailRequest struct {
|
|||||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||||
// @Router /proxmox/tail [get]
|
// @Router /proxmox/tail [get]
|
||||||
func Tail(c *gin.Context) {
|
func Tail(c *gin.Context) {
|
||||||
if !httpheaders.IsWebsocket(c.Request.Header) {
|
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("websocket required"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var request TailRequest
|
var request TailRequest
|
||||||
if err := c.ShouldBindQuery(&request); err != nil {
|
if err := c.ShouldBindQuery(&request); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
|
|||||||
28
internal/api/v1/reload.go
Normal file
28
internal/api/v1/reload.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yusing/godoxy/internal/config"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @x-id "reload"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Reload config
|
||||||
|
// @Description Reload config
|
||||||
|
// @Tags v1
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} apitypes.SuccessResponse
|
||||||
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
|
// @Router /reload [post]
|
||||||
|
func Reload(c *gin.Context) {
|
||||||
|
if err := config.Reload(); err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to reload config"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, apitypes.Success("config reloaded"))
|
||||||
|
}
|
||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
|
||||||
"github.com/yusing/godoxy/internal/route"
|
"github.com/yusing/godoxy/internal/route"
|
||||||
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
|
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RoutesByProvider map[string][]route.Route
|
type RoutesByProvider map[string][]route.Route
|
||||||
@@ -24,10 +24,5 @@ type RoutesByProvider map[string][]route.Route
|
|||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /route/by_provider [get]
|
// @Router /route/by_provider [get]
|
||||||
func ByProvider(c *gin.Context) {
|
func ByProvider(c *gin.Context) {
|
||||||
ep := entrypoint.FromCtx(c.Request.Context())
|
c.JSON(http.StatusOK, routes.ByProvider())
|
||||||
if ep == nil { // impossible, but just in case
|
|
||||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, ep.RoutesByProvider())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package routeApi
|
package routeApi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -55,15 +54,16 @@ type PlaygroundResponse struct {
|
|||||||
MatchedRules []string `json:"matchedRules"`
|
MatchedRules []string `json:"matchedRules"`
|
||||||
FinalRequest FinalRequest `json:"finalRequest"`
|
FinalRequest FinalRequest `json:"finalRequest"`
|
||||||
FinalResponse FinalResponse `json:"finalResponse"`
|
FinalResponse FinalResponse `json:"finalResponse"`
|
||||||
ExecutionError error `json:"executionError,omitempty"` // we need the structured error, not the plain string
|
ExecutionError gperr.Error `json:"executionError,omitempty"`
|
||||||
UpstreamCalled bool `json:"upstreamCalled"`
|
UpstreamCalled bool `json:"upstreamCalled"`
|
||||||
} // @name PlaygroundResponse
|
} // @name PlaygroundResponse
|
||||||
|
|
||||||
type ParsedRule struct {
|
type ParsedRule struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
On string `json:"on"`
|
On string `json:"on"`
|
||||||
Do string `json:"do"`
|
Do string `json:"do"`
|
||||||
ValidationError error `json:"validationError,omitempty"` // we need the structured error, not the plain string
|
ValidationError gperr.Error `json:"validationError,omitempty"`
|
||||||
|
IsResponseRule bool `json:"isResponseRule"`
|
||||||
} // @name ParsedRule
|
} // @name ParsedRule
|
||||||
|
|
||||||
type FinalRequest struct {
|
type FinalRequest struct {
|
||||||
@@ -138,7 +138,7 @@ func Playground(c *gin.Context) {
|
|||||||
// Execute rules
|
// Execute rules
|
||||||
matchedRules := []string{}
|
matchedRules := []string{}
|
||||||
upstreamCalled := false
|
upstreamCalled := false
|
||||||
var executionError error
|
var executionError gperr.Error
|
||||||
|
|
||||||
// Variables to capture modified request state
|
// Variables to capture modified request state
|
||||||
var finalReqMethod, finalReqPath, finalReqHost string
|
var finalReqMethod, finalReqPath, finalReqHost string
|
||||||
@@ -244,22 +244,20 @@ func Playground(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, response)
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlerWithRecover(w http.ResponseWriter, r *http.Request, h http.HandlerFunc, outErr *error) {
|
func handlerWithRecover(w http.ResponseWriter, r *http.Request, h http.HandlerFunc, outErr *gperr.Error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
if outErr != nil {
|
if outErr != nil {
|
||||||
*outErr = fmt.Errorf("panic during rule execution: %v", r)
|
*outErr = gperr.Errorf("panic during rule execution: %v", r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
h(w, r)
|
h(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
|
func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, gperr.Error) {
|
||||||
parsedRules := make([]ParsedRule, 0, len(rawRules))
|
var parsedRules []ParsedRule
|
||||||
rulesList := make(rules.Rules, 0, len(rawRules))
|
var rulesList rules.Rules
|
||||||
|
|
||||||
var valErrs gperr.Builder
|
|
||||||
|
|
||||||
// Parse each rule individually to capture per-rule errors
|
// Parse each rule individually to capture per-rule errors
|
||||||
for _, rawRule := range rawRules {
|
for _, rawRule := range rawRules {
|
||||||
@@ -286,17 +284,14 @@ func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
|
|||||||
|
|
||||||
// Determine if valid
|
// Determine if valid
|
||||||
isValid := onErr == nil && doErr == nil
|
isValid := onErr == nil && doErr == nil
|
||||||
var validationErr error
|
validationErr := gperr.Join(gperr.PrependSubject("on", onErr), gperr.PrependSubject("do", doErr))
|
||||||
if !isValid {
|
|
||||||
validationErr = gperr.Join(gperr.PrependSubject(onErr, "on"), gperr.PrependSubject(doErr, "do"))
|
|
||||||
valErrs.Add(validationErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedRules = append(parsedRules, ParsedRule{
|
parsedRules = append(parsedRules, ParsedRule{
|
||||||
Name: name,
|
Name: name,
|
||||||
On: onStr,
|
On: onStr,
|
||||||
Do: doStr,
|
Do: doStr,
|
||||||
ValidationError: validationErr,
|
ValidationError: validationErr,
|
||||||
|
IsResponseRule: rule.IsResponseRule(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Only add valid rules to execution list
|
// Only add valid rules to execution list
|
||||||
@@ -305,7 +300,7 @@ func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsedRules, rulesList, valErrs.Error()
|
return parsedRules, rulesList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMockRequest(mock MockRequest) *http.Request {
|
func createMockRequest(mock MockRequest) *http.Request {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func TestPlayground(t *testing.T) {
|
|||||||
if len(resp.MatchedRules) != 1 {
|
if len(resp.MatchedRules) != 1 {
|
||||||
t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules))
|
t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules))
|
||||||
}
|
}
|
||||||
if resp.FinalResponse.StatusCode != http.StatusForbidden {
|
if resp.FinalResponse.StatusCode != 403 {
|
||||||
t.Errorf("expected status 403, got %d", resp.FinalResponse.StatusCode)
|
t.Errorf("expected status 403, got %d", resp.FinalResponse.StatusCode)
|
||||||
}
|
}
|
||||||
if resp.UpstreamCalled {
|
if resp.UpstreamCalled {
|
||||||
@@ -168,7 +168,7 @@ func TestPlayground(t *testing.T) {
|
|||||||
if len(resp.MatchedRules) != 1 {
|
if len(resp.MatchedRules) != 1 {
|
||||||
t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules))
|
t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules))
|
||||||
}
|
}
|
||||||
if resp.FinalResponse.StatusCode != http.StatusMethodNotAllowed {
|
if resp.FinalResponse.StatusCode != 405 {
|
||||||
t.Errorf("expected status 405, got %d", resp.FinalResponse.StatusCode)
|
t.Errorf("expected status 405, got %d", resp.FinalResponse.StatusCode)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -179,7 +179,7 @@ func TestPlayground(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Create request
|
// Create request
|
||||||
body, _ := json.Marshal(tt.request)
|
body, _ := json.Marshal(tt.request)
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/route/playground", bytes.NewReader(body))
|
req := httptest.NewRequest("POST", "/api/v1/route/playground", bytes.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
// Create response recorder
|
// Create response recorder
|
||||||
@@ -214,7 +214,7 @@ func TestPlayground(t *testing.T) {
|
|||||||
func TestPlaygroundInvalidRequest(t *testing.T) {
|
func TestPlaygroundInvalidRequest(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/route/playground", bytes.NewReader([]byte(`{}`)))
|
req := httptest.NewRequest("POST", "/api/v1/route/playground", bytes.NewReader([]byte(`{}`)))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,13 +32,7 @@ func Route(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ep := entrypoint.FromCtx(c.Request.Context())
|
route, ok := routes.GetIncludeExcluded(request.Which)
|
||||||
if ep == nil { // impossible, but just in case
|
|
||||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
route, ok := ep.GetRoute(request.Which)
|
|
||||||
if ok {
|
if ok {
|
||||||
c.JSON(http.StatusOK, route)
|
c.JSON(http.StatusOK, route)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
|
||||||
"github.com/yusing/godoxy/internal/route"
|
"github.com/yusing/godoxy/internal/route"
|
||||||
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
type RouteType route.Route // @name Route
|
type RouteType route.Route // @name Route
|
||||||
|
|
||||||
// @x-id "list"
|
// @x-id "routes"
|
||||||
// @BasePath /api/v1
|
// @BasePath /api/v1
|
||||||
// @Summary List routes
|
// @Summary List routes
|
||||||
// @Description List routes
|
// @Description List routes
|
||||||
@@ -32,16 +32,14 @@ func Routes(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ep := entrypoint.FromCtx(c.Request.Context())
|
|
||||||
|
|
||||||
provider := c.Query("provider")
|
provider := c.Query("provider")
|
||||||
if provider == "" {
|
if provider == "" {
|
||||||
c.JSON(http.StatusOK, slices.Collect(ep.IterRoutes))
|
c.JSON(http.StatusOK, slices.Collect(routes.IterAll))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rts := make([]types.Route, 0, ep.NumRoutes())
|
rts := make([]types.Route, 0, routes.NumAllRoutes())
|
||||||
for r := range ep.IterRoutes {
|
for r := range routes.IterAll {
|
||||||
if r.ProviderName() == provider {
|
if r.ProviderName() == provider {
|
||||||
rts = append(rts, r)
|
rts = append(rts, r)
|
||||||
}
|
}
|
||||||
@@ -50,19 +48,17 @@ func Routes(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RoutesWS(c *gin.Context) {
|
func RoutesWS(c *gin.Context) {
|
||||||
ep := entrypoint.FromCtx(c.Request.Context())
|
|
||||||
|
|
||||||
provider := c.Query("provider")
|
provider := c.Query("provider")
|
||||||
if provider == "" {
|
if provider == "" {
|
||||||
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
||||||
return slices.Collect(ep.IterRoutes), nil
|
return slices.Collect(routes.IterAll), nil
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
||||||
rts := make([]types.Route, 0, ep.NumRoutes())
|
rts := make([]types.Route, 0, routes.NumAllRoutes())
|
||||||
for r := range ep.IterRoutes {
|
for r := range routes.IterAll {
|
||||||
if r.ProviderName() == provider {
|
if r.ProviderName() == provider {
|
||||||
rts = append(rts, r)
|
rts = append(rts, r)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# internal/auth
|
# Authentication
|
||||||
|
|
||||||
Authentication providers supporting OIDC and username/password authentication with JWT-based sessions.
|
Authentication providers supporting OIDC and username/password authentication with JWT-based sessions.
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func (auth *OIDCProvider) setSessionTokenCookie(w http.ResponseWriter, r *http.R
|
|||||||
|
|
||||||
func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionClaims, valid bool, err error) {
|
func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionClaims, valid bool, err error) {
|
||||||
claims = &sessionClaims{}
|
claims = &sessionClaims{}
|
||||||
sessionToken, err := jwt.ParseWithClaims(sessionJWT, claims, func(t *jwt.Token) (any, error) {
|
sessionToken, err := jwt.ParseWithClaims(sessionJWT, claims, func(t *jwt.Token) (interface{}, error) {
|
||||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
"github.com/yusing/godoxy/internal/utils"
|
"github.com/yusing/godoxy/internal/utils"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
httputils "github.com/yusing/goutils/http"
|
httputils "github.com/yusing/goutils/http"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
@@ -75,8 +76,8 @@ const (
|
|||||||
var (
|
var (
|
||||||
errMissingIDToken = errors.New("missing id_token field from oauth token")
|
errMissingIDToken = errors.New("missing id_token field from oauth token")
|
||||||
|
|
||||||
ErrMissingOAuthToken = errors.New("missing oauth token")
|
ErrMissingOAuthToken = gperr.New("missing oauth token")
|
||||||
ErrInvalidOAuthToken = errors.New("invalid oauth token")
|
ErrInvalidOAuthToken = gperr.New("invalid oauth token")
|
||||||
)
|
)
|
||||||
|
|
||||||
// generateState generates a random string for OIDC state.
|
// generateState generates a random string for OIDC state.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@@ -9,12 +8,16 @@ import (
|
|||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
httputils "github.com/yusing/goutils/http"
|
httputils "github.com/yusing/goutils/http"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrInvalidUsername = errors.New("invalid username")
|
var (
|
||||||
|
ErrInvalidUsername = gperr.New("invalid username")
|
||||||
|
ErrInvalidPassword = gperr.New("invalid password")
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
UserPassAuth struct {
|
UserPassAuth struct {
|
||||||
@@ -24,9 +27,8 @@ type (
|
|||||||
tokenTTL time.Duration
|
tokenTTL time.Duration
|
||||||
}
|
}
|
||||||
UserPassClaims struct {
|
UserPassClaims struct {
|
||||||
jwt.RegisteredClaims
|
|
||||||
|
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,7 +81,7 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
|||||||
return ErrMissingSessionToken
|
return ErrMissingSessionToken
|
||||||
}
|
}
|
||||||
var claims UserPassClaims
|
var claims UserPassClaims
|
||||||
token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (any, error) {
|
token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (interface{}, error) {
|
||||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
}
|
}
|
||||||
@@ -92,9 +94,9 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
|||||||
case !token.Valid:
|
case !token.Valid:
|
||||||
return ErrInvalidSessionToken
|
return ErrInvalidSessionToken
|
||||||
case claims.Username != auth.username:
|
case claims.Username != auth.username:
|
||||||
return fmt.Errorf("%w: %s", ErrUserNotAllowed, claims.Username)
|
return ErrUserNotAllowed.Subject(claims.Username)
|
||||||
case claims.ExpiresAt.Before(time.Now()):
|
case claims.ExpiresAt.Before(time.Now()):
|
||||||
return fmt.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
|
return gperr.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -137,12 +139,11 @@ func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (auth *UserPassAuth) validatePassword(user, pass string) error {
|
func (auth *UserPassAuth) validatePassword(user, pass string) error {
|
||||||
// always perform bcrypt comparison to avoid timing attacks
|
|
||||||
if err := bcrypt.CompareHashAndPassword(auth.pwdHash, []byte(pass)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if user != auth.username {
|
if user != auth.username {
|
||||||
return ErrInvalidUsername
|
return ErrInvalidUsername.Subject(user)
|
||||||
|
}
|
||||||
|
if err := bcrypt.CompareHashAndPassword(auth.pwdHash, []byte(pass)); err != nil {
|
||||||
|
return ErrInvalidPassword.With(err).Subject(pass)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestUserPassValidateCredentials(t *testing.T) {
|
|||||||
err := auth.validatePassword("username", "password")
|
err := auth.validatePassword("username", "password")
|
||||||
expect.NoError(t, err)
|
expect.NoError(t, err)
|
||||||
err = auth.validatePassword("username", "wrong-password")
|
err = auth.validatePassword("username", "wrong-password")
|
||||||
expect.ErrorIs(t, bcrypt.ErrMismatchedHashAndPassword, err)
|
expect.ErrorIs(t, ErrInvalidPassword, err)
|
||||||
err = auth.validatePassword("wrong-username", "password")
|
err = auth.validatePassword("wrong-username", "password")
|
||||||
expect.ErrorIs(t, ErrInvalidUsername, err)
|
expect.ErrorIs(t, ErrInvalidUsername, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
strutils "github.com/yusing/goutils/strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrMissingSessionToken = errors.New("missing session token")
|
ErrMissingSessionToken = gperr.New("missing session token")
|
||||||
ErrInvalidSessionToken = errors.New("invalid session token")
|
ErrInvalidSessionToken = gperr.New("invalid session token")
|
||||||
ErrUserNotAllowed = errors.New("user not allowed")
|
ErrUserNotAllowed = gperr.New("user not allowed")
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsFrontend(r *http.Request) bool {
|
func IsFrontend(r *http.Request) bool {
|
||||||
@@ -69,12 +70,12 @@ func cookieDomain(r *http.Request) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(reqHost, ".")
|
parts := strutils.SplitRune(reqHost, '.')
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
parts[0] = ""
|
parts[0] = ""
|
||||||
return strings.Join(parts, ".")
|
return strutils.JoinRune(parts, '.')
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {
|
func SetTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# internal/autocert
|
# Autocert Package
|
||||||
|
|
||||||
Automated SSL certificate management using the ACME protocol (Let's Encrypt and compatible CAs).
|
Automated SSL certificate management using the ACME protocol (Let's Encrypt and compatible CAs).
|
||||||
|
|
||||||
|
|||||||
@@ -23,35 +23,33 @@ import (
|
|||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type ConfigExtra Config
|
||||||
ConfigExtra Config
|
type Config struct {
|
||||||
Config struct {
|
Email string `json:"email,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Domains []string `json:"domains,omitempty"`
|
||||||
Domains []string `json:"domains,omitempty"`
|
CertPath string `json:"cert_path,omitempty"`
|
||||||
CertPath string `json:"cert_path,omitempty"`
|
KeyPath string `json:"key_path,omitempty"`
|
||||||
KeyPath string `json:"key_path,omitempty"`
|
Extra []ConfigExtra `json:"extra,omitempty"`
|
||||||
Extra []ConfigExtra `json:"extra,omitempty"`
|
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers with the same CA directory URL
|
||||||
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers with the same CA directory URL
|
Provider string `json:"provider,omitempty"`
|
||||||
Provider string `json:"provider,omitempty"`
|
Options map[string]strutils.Redacted `json:"options,omitempty"`
|
||||||
Options map[string]strutils.Redacted `json:"options,omitempty"`
|
|
||||||
|
|
||||||
Resolvers []string `json:"resolvers,omitempty"`
|
Resolvers []string `json:"resolvers,omitempty"`
|
||||||
|
|
||||||
// Custom ACME CA
|
// Custom ACME CA
|
||||||
CADirURL string `json:"ca_dir_url,omitempty"`
|
CADirURL string `json:"ca_dir_url,omitempty"`
|
||||||
CACerts []string `json:"ca_certs,omitempty"`
|
CACerts []string `json:"ca_certs,omitempty"`
|
||||||
|
|
||||||
// EAB
|
// EAB
|
||||||
EABKid string `json:"eab_kid,omitempty" validate:"required_with=EABHmac"`
|
EABKid string `json:"eab_kid,omitempty" validate:"required_with=EABHmac"`
|
||||||
EABHmac string `json:"eab_hmac,omitempty" validate:"required_with=EABKid"` // base64 encoded
|
EABHmac string `json:"eab_hmac,omitempty" validate:"required_with=EABKid"` // base64 encoded
|
||||||
|
|
||||||
HTTPClient *http.Client `json:"-"` // for tests only
|
HTTPClient *http.Client `json:"-"` // for tests only
|
||||||
|
|
||||||
challengeProvider challenge.Provider
|
challengeProvider challenge.Provider
|
||||||
|
|
||||||
idx int // 0: main, 1+: extra[i]
|
idx int // 0: main, 1+: extra[i]
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrMissingField = gperr.New("missing field")
|
ErrMissingField = gperr.New("missing field")
|
||||||
@@ -68,13 +66,13 @@ const (
|
|||||||
|
|
||||||
var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
|
var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
|
||||||
|
|
||||||
// Validate implements the serialization.CustomValidator interface.
|
// Validate implements the utils.CustomValidator interface.
|
||||||
func (cfg *Config) Validate() error {
|
func (cfg *Config) Validate() gperr.Error {
|
||||||
seenPaths := make(map[string]int) // path -> provider idx (0 for main, 1+ for extras)
|
seenPaths := make(map[string]int) // path -> provider idx (0 for main, 1+ for extras)
|
||||||
return cfg.validate(seenPaths)
|
return cfg.validate(seenPaths)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *ConfigExtra) Validate() error {
|
func (cfg *ConfigExtra) Validate() gperr.Error {
|
||||||
return nil // done by main config's validate
|
return nil // done by main config's validate
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +80,7 @@ func (cfg *ConfigExtra) AsConfig() *Config {
|
|||||||
return (*Config)(cfg)
|
return (*Config)(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) validate(seenPaths map[string]int) error {
|
func (cfg *Config) validate(seenPaths map[string]int) gperr.Error {
|
||||||
if cfg.Provider == "" {
|
if cfg.Provider == "" {
|
||||||
cfg.Provider = ProviderLocal
|
cfg.Provider = ProviderLocal
|
||||||
}
|
}
|
||||||
@@ -159,7 +157,7 @@ func (cfg *Config) validate(seenPaths map[string]int) error {
|
|||||||
cfg.Extra[i].AsConfig().idx = i + 1
|
cfg.Extra[i].AsConfig().idx = i + 1
|
||||||
err := cfg.Extra[i].AsConfig().validate(seenPaths)
|
err := cfg.Extra[i].AsConfig().validate(seenPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.AddSubjectf(err, "extra[%d]", i)
|
b.Add(err.Subjectf("extra[%d]", i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,10 +179,10 @@ func (cfg *Config) GetLegoConfig() (*User, *lego.Config, error) {
|
|||||||
log.Info().Err(err).Msg("failed to load ACME private key, generating a now one")
|
log.Info().Err(err).Msg("failed to load ACME private key, generating a now one")
|
||||||
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("generate ACME private key: %w", err)
|
return nil, nil, gperr.New("generate ACME private key").With(err)
|
||||||
}
|
}
|
||||||
if err = cfg.SaveACMEKey(privKey); err != nil {
|
if err = cfg.SaveACMEKey(privKey); err != nil {
|
||||||
return nil, nil, fmt.Errorf("save ACME private key: %w", err)
|
return nil, nil, gperr.New("save ACME private key").With(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,7 +206,7 @@ func (cfg *Config) GetLegoConfig() (*User, *lego.Config, error) {
|
|||||||
if len(cfg.CACerts) > 0 {
|
if len(cfg.CACerts) > 0 {
|
||||||
certPool, err := lego.CreateCertPool(cfg.CACerts, true)
|
certPool, err := lego.CreateCertPool(cfg.CACerts, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to create cert pool: %w", err)
|
return nil, nil, gperr.New("failed to create cert pool").With(err)
|
||||||
}
|
}
|
||||||
legoCfg.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = certPool
|
legoCfg.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = certPool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
"github.com/go-acme/lego/v4/registration"
|
"github.com/go-acme/lego/v4/registration"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
autocert "github.com/yusing/godoxy/internal/autocert/types"
|
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
@@ -57,6 +56,15 @@ type (
|
|||||||
|
|
||||||
CertExpiries map[string]time.Time
|
CertExpiries map[string]time.Time
|
||||||
|
|
||||||
|
CertInfo struct {
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
NotBefore int64 `json:"not_before"`
|
||||||
|
NotAfter int64 `json:"not_after"`
|
||||||
|
DNSNames []string `json:"dns_names"`
|
||||||
|
EmailAddresses []string `json:"email_addresses"`
|
||||||
|
} // @name CertInfo
|
||||||
|
|
||||||
RenewMode uint8
|
RenewMode uint8
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,6 +82,9 @@ const (
|
|||||||
renewModeIfNeeded
|
renewModeIfNeeded
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// could be nil
|
||||||
|
var ActiveProvider atomic.Pointer[Provider]
|
||||||
|
|
||||||
func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) (*Provider, error) {
|
func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) (*Provider, error) {
|
||||||
p := &Provider{
|
p := &Provider{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@@ -108,14 +119,14 @@ func (p *Provider) GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
|
|||||||
return p.tlsCert, nil
|
return p.tlsCert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) GetCertInfos() ([]autocert.CertInfo, error) {
|
func (p *Provider) GetCertInfos() ([]CertInfo, error) {
|
||||||
allProviders := p.allProviders()
|
allProviders := p.allProviders()
|
||||||
certInfos := make([]autocert.CertInfo, 0, len(allProviders))
|
certInfos := make([]CertInfo, 0, len(allProviders))
|
||||||
for _, provider := range allProviders {
|
for _, provider := range allProviders {
|
||||||
if provider.tlsCert == nil {
|
if provider.tlsCert == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
certInfos = append(certInfos, autocert.CertInfo{
|
certInfos = append(certInfos, CertInfo{
|
||||||
Subject: provider.tlsCert.Leaf.Subject.CommonName,
|
Subject: provider.tlsCert.Leaf.Subject.CommonName,
|
||||||
Issuer: provider.tlsCert.Leaf.Issuer.CommonName,
|
Issuer: provider.tlsCert.Leaf.Issuer.CommonName,
|
||||||
NotBefore: provider.tlsCert.Leaf.NotBefore.Unix(),
|
NotBefore: provider.tlsCert.Leaf.NotBefore.Unix(),
|
||||||
@@ -139,7 +150,7 @@ func (p *Provider) GetName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) fmtError(err error) error {
|
func (p *Provider) fmtError(err error) error {
|
||||||
return gperr.PrependSubject(err, "provider: "+p.GetName())
|
return gperr.PrependSubject(fmt.Sprintf("provider: %s", p.GetName()), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) GetCertPath() string {
|
func (p *Provider) GetCertPath() string {
|
||||||
@@ -205,15 +216,14 @@ func (p *Provider) ObtainCertIfNotExistsAll() error {
|
|||||||
for _, provider := range p.allProviders() {
|
for _, provider := range p.allProviders() {
|
||||||
errs.Go(func() error {
|
errs.Go(func() error {
|
||||||
if err := provider.obtainCertIfNotExists(); err != nil {
|
if err := provider.obtainCertIfNotExists(); err != nil {
|
||||||
return gperr.PrependSubject(err, provider.GetName())
|
return fmt.Errorf("failed to obtain cert for %s: %w", provider.GetName(), err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
err := errs.Wait().Error()
|
|
||||||
p.rebuildSNIMatcher()
|
p.rebuildSNIMatcher()
|
||||||
return err
|
return errs.Wait().Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
// obtainCertIfNotExists obtains a new certificate for this provider if it does not exist.
|
// obtainCertIfNotExists obtains a new certificate for this provider if it does not exist.
|
||||||
@@ -246,15 +256,12 @@ func (p *Provider) ObtainCertAll() error {
|
|||||||
for _, provider := range p.allProviders() {
|
for _, provider := range p.allProviders() {
|
||||||
errs.Go(func() error {
|
errs.Go(func() error {
|
||||||
if err := provider.obtainCertIfNotExists(); err != nil {
|
if err := provider.obtainCertIfNotExists(); err != nil {
|
||||||
return gperr.PrependSubject(err, provider.GetName())
|
return fmt.Errorf("failed to obtain cert for %s: %w", provider.GetName(), err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return errs.Wait().Error()
|
||||||
err := errs.Wait().Error()
|
|
||||||
p.rebuildSNIMatcher()
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObtainCert renews existing certificate or obtains a new certificate for this provider.
|
// ObtainCert renews existing certificate or obtains a new certificate for this provider.
|
||||||
@@ -464,10 +471,10 @@ func (p *Provider) scheduleRenewal(parent task.Parent) {
|
|||||||
|
|
||||||
renewed, err := p.renew(renewMode)
|
renewed, err := p.renew(renewMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(p.fmtError(err)).Msg("autocert: cert renew failed")
|
gperr.LogWarn("autocert: cert renew failed", p.fmtError(err))
|
||||||
notif.Notify(¬if.LogMessage{
|
notif.Notify(¬if.LogMessage{
|
||||||
Level: zerolog.ErrorLevel,
|
Level: zerolog.ErrorLevel,
|
||||||
Title: "SSL certificate renewal failed for " + p.GetName(),
|
Title: fmt.Sprintf("SSL certificate renewal failed for %s", p.GetName()),
|
||||||
Body: notif.MessageBody(err.Error()),
|
Body: notif.MessageBody(err.Error()),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -477,13 +484,13 @@ func (p *Provider) scheduleRenewal(parent task.Parent) {
|
|||||||
|
|
||||||
notif.Notify(¬if.LogMessage{
|
notif.Notify(¬if.LogMessage{
|
||||||
Level: zerolog.InfoLevel,
|
Level: zerolog.InfoLevel,
|
||||||
Title: "SSL certificate renewed for " + p.GetName(),
|
Title: fmt.Sprintf("SSL certificate renewed for %s", p.GetName()),
|
||||||
Body: notif.ListBody(p.cfg.Domains),
|
Body: notif.ListBody(p.cfg.Domains),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reset on success
|
// Reset on success
|
||||||
if err := p.ClearLastFailure(); err != nil {
|
if err := p.ClearLastFailure(); err != nil {
|
||||||
log.Warn().Err(p.fmtError(err)).Msg("autocert: failed to clear last failure")
|
gperr.LogWarn("autocert: failed to clear last failure", p.fmtError(err))
|
||||||
}
|
}
|
||||||
timer.Reset(time.Until(p.ShouldRenewOn()))
|
timer.Reset(time.Until(p.ShouldRenewOn()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func TestMultipleCertificatesLifecycle(t *testing.T) {
|
|||||||
require.Equal(t, "custom", cfg.Extra[1].Provider)
|
require.Equal(t, "custom", cfg.Extra[1].Provider)
|
||||||
|
|
||||||
/* track cert requests for all configs */
|
/* track cert requests for all configs */
|
||||||
os.MkdirAll("certs", 0o755)
|
os.MkdirAll("certs", 0755)
|
||||||
defer os.RemoveAll("certs")
|
defer os.RemoveAll("certs")
|
||||||
|
|
||||||
err = provider.ObtainCertIfNotExistsAll()
|
err = provider.ObtainCertIfNotExistsAll()
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package autocert
|
|||||||
import (
|
import (
|
||||||
"github.com/go-acme/lego/v4/challenge"
|
"github.com/go-acme/lego/v4/challenge"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Generator func(map[string]strutils.Redacted) (challenge.Provider, error)
|
type Generator func(map[string]strutils.Redacted) (challenge.Provider, gperr.Error)
|
||||||
|
|
||||||
var Providers = make(map[string]Generator)
|
var Providers = make(map[string]Generator)
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ func DNSProvider[CT any, PT challenge.Provider](
|
|||||||
defaultCfg func() *CT,
|
defaultCfg func() *CT,
|
||||||
newProvider func(*CT) (PT, error),
|
newProvider func(*CT) (PT, error),
|
||||||
) Generator {
|
) Generator {
|
||||||
return func(opt map[string]strutils.Redacted) (challenge.Provider, error) {
|
return func(opt map[string]strutils.Redacted) (challenge.Provider, gperr.Error) {
|
||||||
cfg := defaultCfg()
|
cfg := defaultCfg()
|
||||||
if len(opt) > 0 {
|
if len(opt) > 0 {
|
||||||
err := serialization.MapUnmarshalValidate(serialization.ToSerializedObject(opt), &cfg)
|
err := serialization.MapUnmarshalValidate(serialization.ToSerializedObject(opt), &cfg)
|
||||||
@@ -23,6 +24,6 @@ func DNSProvider[CT any, PT challenge.Provider](
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
p, pErr := newProvider(cfg)
|
p, pErr := newProvider(cfg)
|
||||||
return p, pErr
|
return p, gperr.Wrap(pErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Provider) setupExtraProviders() error {
|
func (p *Provider) setupExtraProviders() gperr.Error {
|
||||||
p.sniMatcher = sniMatcher{}
|
p.sniMatcher = sniMatcher{}
|
||||||
if len(p.cfg.Extra) == 0 {
|
if len(p.cfg.Extra) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package autocert
|
|
||||||
|
|
||||||
type CertInfo struct {
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
Issuer string `json:"issuer"`
|
|
||||||
NotBefore int64 `json:"not_before"`
|
|
||||||
NotAfter int64 `json:"not_after"`
|
|
||||||
DNSNames []string `json:"dns_names"`
|
|
||||||
EmailAddresses []string `json:"email_addresses"`
|
|
||||||
} // @name CertInfo
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package autocert
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type ContextKey struct{}
|
|
||||||
|
|
||||||
func SetCtx(ctx interface{ SetValue(key, value any) }, p Provider) {
|
|
||||||
ctx.SetValue(ContextKey{}, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromCtx(ctx context.Context) Provider {
|
|
||||||
if provider, ok := ctx.Value(ContextKey{}).(Provider); ok {
|
|
||||||
return provider
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
package autocert
|
package autocert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
|
Setup() error
|
||||||
GetCertInfos() ([]CertInfo, error)
|
GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||||
ScheduleRenewalAll(parent task.Parent)
|
ScheduleRenewalAll(task.Parent)
|
||||||
ObtainCertAll() error
|
ObtainCertAll() error
|
||||||
ForceExpiryAll() bool
|
|
||||||
WaitRenewalDone(ctx context.Context) bool
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# internal/config
|
# Configuration Management
|
||||||
|
|
||||||
Centralized YAML configuration management with thread-safe state access and provider initialization.
|
Centralized YAML configuration management with thread-safe state access and provider initialization.
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ type State interface {
|
|||||||
Task() *task.Task
|
Task() *task.Task
|
||||||
Context() context.Context
|
Context() context.Context
|
||||||
Value() *Config
|
Value() *Config
|
||||||
Entrypoint() entrypoint.Entrypoint
|
EntrypointHandler() http.Handler
|
||||||
ShortLinkMatcher() config.ShortLinkMatcher
|
ShortLinkMatcher() config.ShortLinkMatcher
|
||||||
AutoCertProvider() server.CertProvider
|
AutoCertProvider() server.CertProvider
|
||||||
LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool)
|
LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool)
|
||||||
@@ -62,12 +62,6 @@ type State interface {
|
|||||||
IterProviders() iter.Seq2[string, types.RouteProvider]
|
IterProviders() iter.Seq2[string, types.RouteProvider]
|
||||||
StartProviders() error
|
StartProviders() error
|
||||||
NumProviders() int
|
NumProviders() int
|
||||||
|
|
||||||
// Lifecycle management
|
|
||||||
StartAPIServers()
|
|
||||||
StartMetrics()
|
|
||||||
|
|
||||||
FlushTmpLog()
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -220,15 +214,12 @@ Configuration supports hot-reloading via editing `config/config.yml`.
|
|||||||
|
|
||||||
- `internal/acl` - Access control configuration
|
- `internal/acl` - Access control configuration
|
||||||
- `internal/autocert` - SSL certificate management
|
- `internal/autocert` - SSL certificate management
|
||||||
- `internal/entrypoint` - HTTP entrypoint setup (now via interface)
|
- `internal/entrypoint` - HTTP entrypoint setup
|
||||||
- `internal/route/provider` - Route providers (Docker, file, agent)
|
- `internal/route/provider` - Route providers (Docker, file, agent)
|
||||||
- `internal/maxmind` - GeoIP configuration
|
- `internal/maxmind` - GeoIP configuration
|
||||||
- `internal/notif` - Notification providers
|
- `internal/notif` - Notification providers
|
||||||
- `internal/proxmox` - LXC container management
|
- `internal/proxmox` - LXC container management
|
||||||
- `internal/homepage/types` - Dashboard configuration
|
- `internal/homepage/types` - Dashboard configuration
|
||||||
- `internal/api` - REST API servers
|
|
||||||
- `internal/metrics/systeminfo` - System metrics polling
|
|
||||||
- `internal/metrics/uptime` - Uptime tracking
|
|
||||||
- `github.com/yusing/goutils/task` - Object lifecycle management
|
- `github.com/yusing/goutils/task` - Object lifecycle management
|
||||||
|
|
||||||
### External dependencies
|
### External dependencies
|
||||||
@@ -321,8 +312,5 @@ for name, provider := range config.GetState().IterProviders() {
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
state := config.GetState()
|
state := config.GetState()
|
||||||
// Get entrypoint interface for route management
|
http.Handle("/", state.EntrypointHandler())
|
||||||
ep := state.Entrypoint()
|
|
||||||
// Add routes directly to entrypoint
|
|
||||||
ep.AddRoute(route)
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -7,15 +7,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
"github.com/yusing/godoxy/internal/watcher"
|
"github.com/yusing/godoxy/internal/watcher"
|
||||||
watcherEvents "github.com/yusing/godoxy/internal/watcher/events"
|
"github.com/yusing/godoxy/internal/watcher/events"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
"github.com/yusing/goutils/eventqueue"
|
"github.com/yusing/goutils/server"
|
||||||
"github.com/yusing/goutils/events"
|
|
||||||
"github.com/yusing/goutils/strings/ansi"
|
"github.com/yusing/goutils/strings/ansi"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
@@ -27,29 +26,29 @@ var (
|
|||||||
|
|
||||||
const configEventFlushInterval = 500 * time.Millisecond
|
const configEventFlushInterval = 500 * time.Millisecond
|
||||||
|
|
||||||
var (
|
const (
|
||||||
errCfgRenameWarn = errors.New("config file renamed, not reloading; Make sure you rename it back before next time you start")
|
cfgRenameWarn = `Config file renamed, not reloading.
|
||||||
errCfgDeleteWarn = errors.New(`config file deleted, not reloading; You may run "ls-config" to show or dump the current config`)
|
Make sure you rename it back before next time you start.`
|
||||||
|
cfgDeleteWarn = `Config file deleted, not reloading.
|
||||||
|
You may run "ls-config" to show or dump the current config.`
|
||||||
)
|
)
|
||||||
|
|
||||||
func logNotifyError(action string, err error) {
|
func logNotifyError(action string, err error) {
|
||||||
log.Error().Err(err).Msg("config " + action + " error")
|
gperr.LogError("config "+action+" error", err)
|
||||||
notif.Notify(¬if.LogMessage{
|
notif.Notify(¬if.LogMessage{
|
||||||
Level: zerolog.ErrorLevel,
|
Level: zerolog.ErrorLevel,
|
||||||
Title: fmt.Sprintf("Config %s error", action),
|
Title: fmt.Sprintf("Config %s error", action),
|
||||||
Body: notif.ErrorBody(err),
|
Body: notif.ErrorBody(err),
|
||||||
})
|
})
|
||||||
events.Global.Add(events.NewEvent(events.LevelError, "config", action, err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func logNotifyWarn(action string, err error) {
|
func logNotifyWarn(action string, err error) {
|
||||||
log.Warn().Err(err).Msg("config " + action + " warning")
|
gperr.LogWarn("config "+action+" error", err)
|
||||||
notif.Notify(¬if.LogMessage{
|
notif.Notify(¬if.LogMessage{
|
||||||
Level: zerolog.WarnLevel,
|
Level: zerolog.WarnLevel,
|
||||||
Title: fmt.Sprintf("Config %s warning", action),
|
Title: fmt.Sprintf("Config %s warning", action),
|
||||||
Body: notif.ErrorBody(err),
|
Body: notif.ErrorBody(err),
|
||||||
})
|
})
|
||||||
events.Global.Add(events.NewEvent(events.LevelWarn, "config", action, err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() error {
|
func Load() error {
|
||||||
@@ -61,29 +60,20 @@ func Load() error {
|
|||||||
|
|
||||||
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
|
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
|
||||||
|
|
||||||
initErr := state.InitFromFile(common.ConfigPath)
|
|
||||||
if initErr != nil {
|
|
||||||
// if error is critical, notify and return it without starting providers
|
|
||||||
if criticalErr, ok := errors.AsType[CriticalError](initErr); ok {
|
|
||||||
logNotifyError("init", criticalErr.err)
|
|
||||||
return criticalErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// disable pool logging temporary since we already have pretty logging
|
// disable pool logging temporary since we already have pretty logging
|
||||||
state.Entrypoint().DisablePoolsLog(true)
|
routes.HTTP.DisableLog(true)
|
||||||
|
routes.Stream.DisableLog(true)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
state.Entrypoint().DisablePoolsLog(false)
|
routes.HTTP.DisableLog(false)
|
||||||
|
routes.Stream.DisableLog(false)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
initErr := state.InitFromFile(common.ConfigPath)
|
||||||
err := errors.Join(initErr, state.StartProviders())
|
err := errors.Join(initErr, state.StartProviders())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logNotifyError("init", err)
|
logNotifyError("init", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.StartAPIServers()
|
|
||||||
state.StartMetrics()
|
|
||||||
|
|
||||||
SetState(state)
|
SetState(state)
|
||||||
|
|
||||||
// flush temporary log
|
// flush temporary log
|
||||||
@@ -91,9 +81,7 @@ func Load() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Reload() error {
|
func Reload() gperr.Error {
|
||||||
events.Global.Add(events.NewEvent(events.LevelInfo, "config", "reload", nil))
|
|
||||||
|
|
||||||
// avoid race between config change and API reload request
|
// avoid race between config change and API reload request
|
||||||
reloadMu.Lock()
|
reloadMu.Lock()
|
||||||
defer reloadMu.Unlock()
|
defer reloadMu.Unlock()
|
||||||
@@ -120,35 +108,32 @@ func Reload() error {
|
|||||||
logNotifyError("start providers", err)
|
logNotifyError("start providers", err)
|
||||||
return nil // continue
|
return nil // continue
|
||||||
}
|
}
|
||||||
|
StartProxyServers()
|
||||||
newState.StartAPIServers()
|
|
||||||
newState.StartMetrics()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func WatchChanges() {
|
func WatchChanges() {
|
||||||
opts := eventqueue.Options[watcherEvents.Event]{
|
t := task.RootTask("config_watcher", true)
|
||||||
FlushInterval: configEventFlushInterval,
|
eventQueue := events.NewEventQueue(
|
||||||
OnFlush: OnConfigChange,
|
t,
|
||||||
OnError: func(err error) {
|
configEventFlushInterval,
|
||||||
|
OnConfigChange,
|
||||||
|
func(err gperr.Error) {
|
||||||
logNotifyError("reload", err)
|
logNotifyError("reload", err)
|
||||||
},
|
},
|
||||||
Debug: common.IsDebug,
|
)
|
||||||
}
|
|
||||||
t := task.RootTask("config_watcher", true)
|
|
||||||
eventQueue := eventqueue.New(t, opts)
|
|
||||||
eventQueue.Start(cfgWatcher.Events(t.Context()))
|
eventQueue.Start(cfgWatcher.Events(t.Context()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func OnConfigChange(ev []watcherEvents.Event) {
|
func OnConfigChange(ev []events.Event) {
|
||||||
// no matter how many events during the interval
|
// no matter how many events during the interval
|
||||||
// just reload once and check the last event
|
// just reload once and check the last event
|
||||||
switch ev[len(ev)-1].Action {
|
switch ev[len(ev)-1].Action {
|
||||||
case watcherEvents.ActionFileRenamed:
|
case events.ActionFileRenamed:
|
||||||
logNotifyWarn("rename", errCfgRenameWarn)
|
logNotifyWarn("rename", errors.New(cfgRenameWarn))
|
||||||
return
|
return
|
||||||
case watcherEvents.ActionFileDeleted:
|
case events.ActionFileDeleted:
|
||||||
logNotifyWarn("delete", errCfgDeleteWarn)
|
logNotifyWarn("delete", errors.New(cfgDeleteWarn))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,3 +142,16 @@ func OnConfigChange(ev []watcherEvents.Event) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StartProxyServers() {
|
||||||
|
cfg := GetState()
|
||||||
|
server.StartServer(cfg.Task(), server.Options{
|
||||||
|
Name: "proxy",
|
||||||
|
CertProvider: cfg.AutoCertProvider(),
|
||||||
|
HTTPAddr: common.ProxyHTTPAddr,
|
||||||
|
HTTPSAddr: common.ProxyHTTPSAddr,
|
||||||
|
Handler: cfg.EntrypointHandler(),
|
||||||
|
ACL: cfg.Value().ACL,
|
||||||
|
SupportProxyProtocol: cfg.Value().Entrypoint.SupportProxyProtocol,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# internal/config/query
|
# Configuration Query
|
||||||
|
|
||||||
Read-only access to the active configuration state, including route providers and system statistics.
|
Read-only access to the active configuration state, including route providers and system statistics.
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ No metrics are currently exposed.
|
|||||||
## Performance Characteristics
|
## Performance Characteristics
|
||||||
|
|
||||||
- O(n) where n is number of providers for provider queries
|
- O(n) where n is number of providers for provider queries
|
||||||
- O(n \* m) where m is routes per provider for route search
|
- O(n * m) where m is routes per provider for route search
|
||||||
- O(n) for statistics aggregation
|
- O(n) for statistics aggregation
|
||||||
- No locking required (uses atomic load)
|
- No locking required (uses atomic load)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"iter"
|
"iter"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -17,21 +18,14 @@ import (
|
|||||||
"github.com/goccy/go-yaml"
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/puzpuzpuz/xsync/v4"
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/yusing/godoxy/internal/acl"
|
||||||
acl "github.com/yusing/godoxy/internal/acl/types"
|
|
||||||
"github.com/yusing/godoxy/internal/agentpool"
|
"github.com/yusing/godoxy/internal/agentpool"
|
||||||
"github.com/yusing/godoxy/internal/api"
|
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
autocertctx "github.com/yusing/godoxy/internal/autocert/types"
|
|
||||||
"github.com/yusing/godoxy/internal/common"
|
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
"github.com/yusing/godoxy/internal/entrypoint"
|
"github.com/yusing/godoxy/internal/entrypoint"
|
||||||
entrypointctx "github.com/yusing/godoxy/internal/entrypoint/types"
|
|
||||||
homepage "github.com/yusing/godoxy/internal/homepage/types"
|
homepage "github.com/yusing/godoxy/internal/homepage/types"
|
||||||
"github.com/yusing/godoxy/internal/logging"
|
"github.com/yusing/godoxy/internal/logging"
|
||||||
"github.com/yusing/godoxy/internal/maxmind"
|
"github.com/yusing/godoxy/internal/maxmind"
|
||||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
|
||||||
"github.com/yusing/godoxy/internal/metrics/uptime"
|
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
route "github.com/yusing/godoxy/internal/route/provider"
|
route "github.com/yusing/godoxy/internal/route/provider"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
@@ -46,7 +40,7 @@ type state struct {
|
|||||||
|
|
||||||
providers *xsync.Map[string, types.RouteProvider]
|
providers *xsync.Map[string, types.RouteProvider]
|
||||||
autocertProvider *autocert.Provider
|
autocertProvider *autocert.Provider
|
||||||
entrypoint *entrypoint.Entrypoint
|
entrypoint entrypoint.Entrypoint
|
||||||
|
|
||||||
task *task.Task
|
task *task.Task
|
||||||
|
|
||||||
@@ -56,25 +50,14 @@ type state struct {
|
|||||||
tmpLog zerolog.Logger
|
tmpLog zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type CriticalError struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e CriticalError) Error() string {
|
|
||||||
return e.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e CriticalError) Unwrap() error {
|
|
||||||
return e.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewState() config.State {
|
func NewState() config.State {
|
||||||
tmpLogBuf := bytes.NewBuffer(make([]byte, 0, 4096))
|
tmpLogBuf := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||||
return &state{
|
return &state{
|
||||||
providers: xsync.NewMap[string, types.RouteProvider](),
|
providers: xsync.NewMap[string, types.RouteProvider](),
|
||||||
task: task.RootTask("config", false),
|
entrypoint: entrypoint.NewEntrypoint(),
|
||||||
tmpLogBuf: tmpLogBuf,
|
task: task.RootTask("config", false),
|
||||||
tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf),
|
tmpLogBuf: tmpLogBuf,
|
||||||
|
tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +73,13 @@ func SetState(state config.State) {
|
|||||||
|
|
||||||
cfg := state.Value()
|
cfg := state.Value()
|
||||||
config.ActiveState.Store(state)
|
config.ActiveState.Store(state)
|
||||||
|
entrypoint.ActiveConfig.Store(&cfg.Entrypoint)
|
||||||
homepage.ActiveConfig.Store(&cfg.Homepage)
|
homepage.ActiveConfig.Store(&cfg.Homepage)
|
||||||
|
if autocertProvider := state.AutoCertProvider(); autocertProvider != nil {
|
||||||
|
autocert.ActiveProvider.Store(autocertProvider.(*autocert.Provider))
|
||||||
|
} else {
|
||||||
|
autocert.ActiveProvider.Store(nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HasState() bool {
|
func HasState() bool {
|
||||||
@@ -107,7 +96,7 @@ func (state *state) InitFromFile(filename string) error {
|
|||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
state.Config = config.DefaultConfig()
|
state.Config = config.DefaultConfig()
|
||||||
} else {
|
} else {
|
||||||
return CriticalError{err}
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state.Init(data)
|
return state.Init(data)
|
||||||
@@ -116,7 +105,7 @@ func (state *state) InitFromFile(filename string) error {
|
|||||||
func (state *state) Init(data []byte) error {
|
func (state *state) Init(data []byte) error {
|
||||||
err := serialization.UnmarshalValidate(data, &state.Config, yaml.Unmarshal)
|
err := serialization.UnmarshalValidate(data, &state.Config, yaml.Unmarshal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CriticalError{err}
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
g := gperr.NewGroup("config load error")
|
g := gperr.NewGroup("config load error")
|
||||||
@@ -128,9 +117,7 @@ func (state *state) Init(data []byte) error {
|
|||||||
// these won't benefit from running on goroutines
|
// these won't benefit from running on goroutines
|
||||||
errs.Add(state.initNotification())
|
errs.Add(state.initNotification())
|
||||||
errs.Add(state.initACL())
|
errs.Add(state.initACL())
|
||||||
if err := state.initEntrypoint(); err != nil {
|
errs.Add(state.initEntrypoint())
|
||||||
errs.Add(CriticalError{err})
|
|
||||||
}
|
|
||||||
errs.Add(state.loadRouteProviders())
|
errs.Add(state.loadRouteProviders())
|
||||||
return errs.Error()
|
return errs.Error()
|
||||||
}
|
}
|
||||||
@@ -147,8 +134,8 @@ func (state *state) Value() *config.Config {
|
|||||||
return &state.Config
|
return &state.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *state) Entrypoint() entrypointctx.Entrypoint {
|
func (state *state) EntrypointHandler() http.Handler {
|
||||||
return state.entrypoint
|
return &state.entrypoint
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *state) ShortLinkMatcher() config.ShortLinkMatcher {
|
func (state *state) ShortLinkMatcher() config.ShortLinkMatcher {
|
||||||
@@ -199,39 +186,10 @@ func (state *state) NumProviders() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (state *state) FlushTmpLog() {
|
func (state *state) FlushTmpLog() {
|
||||||
_, _ = state.tmpLogBuf.WriteTo(os.Stdout)
|
state.tmpLogBuf.WriteTo(os.Stdout)
|
||||||
state.tmpLogBuf.Reset()
|
state.tmpLogBuf.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *state) StartAPIServers() {
|
|
||||||
// API Handler needs to start after auth is initialized.
|
|
||||||
_, err := server.StartServer(state.task.Subtask("api_server", false), server.Options{
|
|
||||||
Name: "api",
|
|
||||||
HTTPAddr: common.APIHTTPAddr,
|
|
||||||
Handler: api.NewHandler(true),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to start API server")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local API Handler is used for unauthenticated access.
|
|
||||||
if common.LocalAPIHTTPAddr != "" {
|
|
||||||
_, err := server.StartServer(state.task.Subtask("local_api_server", false), server.Options{
|
|
||||||
Name: "local_api",
|
|
||||||
HTTPAddr: common.LocalAPIHTTPAddr,
|
|
||||||
Handler: api.NewHandler(false),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("failed to start local API server")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (state *state) StartMetrics() {
|
|
||||||
systeminfo.Poller.Start(state.task)
|
|
||||||
uptime.Poller.Start(state.task)
|
|
||||||
}
|
|
||||||
|
|
||||||
// initACL initializes the ACL.
|
// initACL initializes the ACL.
|
||||||
func (state *state) initACL() error {
|
func (state *state) initACL() error {
|
||||||
if !state.ACL.Valid() {
|
if !state.ACL.Valid() {
|
||||||
@@ -241,7 +199,7 @@ func (state *state) initACL() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
acl.SetCtx(state.task, state.ACL)
|
state.task.SetValue(acl.ContextKey{}, state.ACL)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +207,6 @@ func (state *state) initEntrypoint() error {
|
|||||||
epCfg := state.Config.Entrypoint
|
epCfg := state.Config.Entrypoint
|
||||||
matchDomains := state.MatchDomains
|
matchDomains := state.MatchDomains
|
||||||
|
|
||||||
state.entrypoint = entrypoint.NewEntrypoint(state.task, &epCfg)
|
|
||||||
state.entrypoint.SetFindRouteDomains(matchDomains)
|
state.entrypoint.SetFindRouteDomains(matchDomains)
|
||||||
state.entrypoint.SetNotFoundRules(epCfg.Rules.NotFound)
|
state.entrypoint.SetNotFoundRules(epCfg.Rules.NotFound)
|
||||||
|
|
||||||
@@ -263,8 +220,6 @@ func (state *state) initEntrypoint() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entrypointctx.SetCtx(state.task, state.entrypoint)
|
|
||||||
|
|
||||||
errs := gperr.NewBuilder("entrypoint error")
|
errs := gperr.NewBuilder("entrypoint error")
|
||||||
errs.Add(state.entrypoint.SetMiddlewares(epCfg.Middlewares))
|
errs.Add(state.entrypoint.SetMiddlewares(epCfg.Middlewares))
|
||||||
errs.Add(state.entrypoint.SetAccessLogger(state.task, epCfg.AccessLog))
|
errs.Add(state.entrypoint.SetAccessLogger(state.task, epCfg.AccessLog))
|
||||||
@@ -341,7 +296,6 @@ func (state *state) initAutoCert() error {
|
|||||||
p.PrintCertExpiriesAll()
|
p.PrintCertExpiriesAll()
|
||||||
|
|
||||||
state.autocertProvider = p
|
state.autocertProvider = p
|
||||||
autocertctx.SetCtx(state.task, p)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,7 +309,7 @@ func (state *state) initProxmox() error {
|
|||||||
for _, cfg := range proxmoxCfg {
|
for _, cfg := range proxmoxCfg {
|
||||||
errs.Go(func() error {
|
errs.Go(func() error {
|
||||||
if err := cfg.Init(state.task.Context()); err != nil {
|
if err := cfg.Init(state.task.Context()); err != nil {
|
||||||
return gperr.PrependSubject(err, cfg.URL)
|
return err.Subject(cfg.URL)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -379,7 +333,7 @@ func (state *state) loadRouteProviders() error {
|
|||||||
for _, a := range providers.Agents {
|
for _, a := range providers.Agents {
|
||||||
agentErrs.Go(func() error {
|
agentErrs.Go(func() error {
|
||||||
if err := a.Init(state.task.Context()); err != nil {
|
if err := a.Init(state.task.Context()); err != nil {
|
||||||
return gperr.PrependSubject(err, a.String())
|
return gperr.PrependSubject(a.String(), err)
|
||||||
}
|
}
|
||||||
agentpool.Add(a)
|
agentpool.Add(a)
|
||||||
return nil
|
return nil
|
||||||
@@ -397,7 +351,7 @@ func (state *state) loadRouteProviders() error {
|
|||||||
for _, filename := range providers.Files {
|
for _, filename := range providers.Files {
|
||||||
p, err := route.NewFileProvider(filename)
|
p, err := route.NewFileProvider(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Add(gperr.PrependSubject(err, filename))
|
errs.Add(gperr.PrependSubject(filename, err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
registerProvider(p)
|
registerProvider(p)
|
||||||
@@ -422,7 +376,7 @@ func (state *state) loadRouteProviders() error {
|
|||||||
for _, p := range state.providers.Range {
|
for _, p := range state.providers.Range {
|
||||||
loadErrs.Go(func() error {
|
loadErrs.Go(func() error {
|
||||||
if err := p.LoadRoutes(); err != nil {
|
if err := p.LoadRoutes(); err != nil {
|
||||||
return gperr.PrependSubject(err, p.String())
|
return err.Subject(p.String())
|
||||||
}
|
}
|
||||||
resultsMu.Lock()
|
resultsMu.Lock()
|
||||||
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
|
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import (
|
|||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/internal/acl"
|
"github.com/yusing/godoxy/internal/acl"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
"github.com/yusing/godoxy/internal/entrypoint"
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
homepage "github.com/yusing/godoxy/internal/homepage/types"
|
homepage "github.com/yusing/godoxy/internal/homepage/types"
|
||||||
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
"github.com/yusing/godoxy/internal/proxmox"
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -41,7 +42,7 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func Validate(data []byte) error {
|
func Validate(data []byte) gperr.Error {
|
||||||
var model Config
|
var model Config
|
||||||
return serialization.UnmarshalValidate(data, &model, yaml.Unmarshal)
|
return serialization.UnmarshalValidate(data, &model, yaml.Unmarshal)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"iter"
|
"iter"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
"github.com/yusing/goutils/server"
|
"github.com/yusing/goutils/server"
|
||||||
"github.com/yusing/goutils/synk"
|
"github.com/yusing/goutils/synk"
|
||||||
@@ -22,7 +21,7 @@ type State interface {
|
|||||||
|
|
||||||
Value() *Config
|
Value() *Config
|
||||||
|
|
||||||
Entrypoint() entrypoint.Entrypoint
|
EntrypointHandler() http.Handler
|
||||||
ShortLinkMatcher() ShortLinkMatcher
|
ShortLinkMatcher() ShortLinkMatcher
|
||||||
AutoCertProvider() server.CertProvider
|
AutoCertProvider() server.CertProvider
|
||||||
|
|
||||||
@@ -33,9 +32,6 @@ type State interface {
|
|||||||
StartProviders() error
|
StartProviders() error
|
||||||
|
|
||||||
FlushTmpLog()
|
FlushTmpLog()
|
||||||
|
|
||||||
StartAPIServers()
|
|
||||||
StartMetrics()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShortLinkMatcher interface {
|
type ShortLinkMatcher interface {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# internal/dnsproviders
|
# DNS Providers
|
||||||
|
|
||||||
DNS provider integrations for Let's Encrypt certificate management via the lego library.
|
DNS provider integrations for Let's Encrypt certificate management via the lego library.
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user