mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-22 08:48:43 +02:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baf774f927 | ||
|
|
a3c82209c6 | ||
|
|
386d946bd2 | ||
|
|
ee9bf31d30 | ||
|
|
2c87eebee3 | ||
|
|
5be784d567 | ||
|
|
a999c51bf8 | ||
|
|
7ca722b256 | ||
|
|
51295be463 | ||
|
|
51fc5f017a | ||
|
|
e4996733fc | ||
|
|
f76d86dfa2 | ||
|
|
8778f4ea73 | ||
|
|
6f75bb7593 | ||
|
|
964ba1eac1 | ||
|
|
6e7b571946 | ||
|
|
fc7a81faf5 | ||
|
|
488ad160e7 | ||
|
|
1ec2872f3d | ||
|
|
9c3346dd9d | ||
|
|
203faa8e7e | ||
|
|
fbc853fa6a | ||
|
|
3fefbdfded | ||
|
|
48be6def12 | ||
|
|
94d6b7a168 | ||
|
|
1ca4b4939e | ||
|
|
f8716d990e | ||
|
|
5a91db8d10 | ||
|
|
3e73be60a1 | ||
|
|
af9363209b | ||
|
|
ccc35b2a00 | ||
|
|
44536139c1 | ||
|
|
2b4c39a79e | ||
|
|
ddf78aacba | ||
|
|
f5a006ce81 | ||
|
|
290af4e311 | ||
|
|
feafdf05f2 | ||
|
|
b09bfd6c1e | ||
|
|
e13b18621d | ||
|
|
53f3397b7a | ||
|
|
19968834d2 | ||
|
|
d41c6f8d77 | ||
|
|
dcc5ab8952 |
@@ -63,9 +63,6 @@ GODOXY_METRICS_DISABLE_DISK=false
|
|||||||
GODOXY_METRICS_DISABLE_NETWORK=false
|
GODOXY_METRICS_DISABLE_NETWORK=false
|
||||||
GODOXY_METRICS_DISABLE_SENSORS=false
|
GODOXY_METRICS_DISABLE_SENSORS=false
|
||||||
|
|
||||||
# Frontend listening port
|
|
||||||
GODOXY_FRONTEND_PORT=3000
|
|
||||||
|
|
||||||
# Frontend aliases (subdomains / FQDNs, e.g. godoxy, godoxy.domain.com)
|
# Frontend aliases (subdomains / FQDNs, e.g. godoxy, godoxy.domain.com)
|
||||||
GODOXY_FRONTEND_ALIASES=godoxy
|
GODOXY_FRONTEND_ALIASES=godoxy
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Stage 1: deps
|
# Stage 1: deps
|
||||||
FROM golang:1.25.2-alpine AS deps
|
FROM golang:1.25.3-alpine AS deps
|
||||||
HEALTHCHECK NONE
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
# package version does not matter
|
# package version does not matter
|
||||||
|
|||||||
24
Makefile
24
Makefile
@@ -3,9 +3,10 @@ export VERSION ?= $(shell git describe --tags --abbrev=0)
|
|||||||
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
|
||||||
|
|
||||||
WEBUI_DIR ?= ../godoxy-frontend
|
WEBUI_DIR ?= ../godoxy-webui
|
||||||
DOCS_DIR ?= ../godoxy-wiki
|
DOCS_DIR ?= ${WEBUI_DIR}/wiki
|
||||||
|
|
||||||
|
GO_TAGS = sonic
|
||||||
LDFLAGS = -X github.com/yusing/goutils/version.version=${VERSION} -checklinkname=0
|
LDFLAGS = -X github.com/yusing/goutils/version.version=${VERSION} -checklinkname=0
|
||||||
|
|
||||||
ifeq ($(agent), 1)
|
ifeq ($(agent), 1)
|
||||||
@@ -28,23 +29,26 @@ endif
|
|||||||
ifeq ($(race), 1)
|
ifeq ($(race), 1)
|
||||||
CGO_ENABLED = 1
|
CGO_ENABLED = 1
|
||||||
GODOXY_DEBUG = 1
|
GODOXY_DEBUG = 1
|
||||||
BUILD_FLAGS += -tags debug -race
|
GO_TAGS += debug
|
||||||
|
BUILD_FLAGS += -race
|
||||||
else ifeq ($(debug), 1)
|
else ifeq ($(debug), 1)
|
||||||
CGO_ENABLED = 1
|
CGO_ENABLED = 1
|
||||||
GODOXY_DEBUG = 1
|
GODOXY_DEBUG = 1
|
||||||
BUILD_FLAGS += -gcflags=all='-N -l' -tags debug -asan
|
GO_TAGS += debug
|
||||||
|
BUILD_FLAGS += -asan # FIXME: -gcflags=all='-N -l'
|
||||||
else ifeq ($(pprof), 1)
|
else ifeq ($(pprof), 1)
|
||||||
CGO_ENABLED = 0
|
CGO_ENABLED = 0
|
||||||
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
||||||
BUILD_FLAGS += -tags pprof
|
GO_TAGS += pprof
|
||||||
VERSION := ${VERSION}-pprof
|
VERSION := ${VERSION}-pprof
|
||||||
else
|
else
|
||||||
CGO_ENABLED = 0
|
CGO_ENABLED = 0
|
||||||
LDFLAGS += -s -w
|
LDFLAGS += -s -w
|
||||||
BUILD_FLAGS += -pgo=auto -tags production
|
GO_TAGS += production
|
||||||
|
BUILD_FLAGS += -pgo=auto
|
||||||
endif
|
endif
|
||||||
|
|
||||||
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
|
BUILD_FLAGS += -tags '$(GO_TAGS)' -ldflags='$(LDFLAGS)'
|
||||||
BIN_PATH := $(shell pwd)/bin/${NAME}
|
BIN_PATH := $(shell pwd)/bin/${NAME}
|
||||||
|
|
||||||
export NAME
|
export NAME
|
||||||
@@ -143,15 +147,17 @@ push-github:
|
|||||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
|
|
||||||
gen-swagger:
|
gen-swagger:
|
||||||
swag init --parseDependency --parseInternal -g handler.go -d internal/api -o internal/api/v1/docs
|
swag init --parseDependency --parseInternal --parseFuncBody -g handler.go -d internal/api -o internal/api/v1/docs
|
||||||
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
|
||||||
|
|
||||||
gen-swagger-markdown: gen-swagger
|
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
|
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
|
||||||
pnpx swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
||||||
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
||||||
|
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
|
||||||
22
agent/go.mod
22
agent/go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/yusing/godoxy/agent
|
module github.com/yusing/godoxy/agent
|
||||||
|
|
||||||
go 1.25.2
|
go 1.25.3
|
||||||
|
|
||||||
replace github.com/yusing/godoxy => ..
|
replace github.com/yusing/godoxy => ..
|
||||||
|
|
||||||
@@ -19,15 +19,17 @@ require (
|
|||||||
github.com/puzpuzpuz/xsync/v4 v4.2.0
|
github.com/puzpuzpuz/xsync/v4 v4.2.0
|
||||||
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.18.6
|
github.com/valyala/fasthttp v1.68.0
|
||||||
|
github.com/yusing/godoxy v0.19.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.6.1
|
github.com/yusing/goutils v0.7.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
@@ -55,12 +57,13 @@ require (
|
|||||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/gotify/server/v2 v2.7.3 // indirect
|
github.com/gotify/server/v2 v2.7.3 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // 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.1 // 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/lithammer/fuzzysearch v1.1.8 // indirect
|
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // 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
|
||||||
@@ -78,24 +81,25 @@ require (
|
|||||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||||
github.com/samber/lo v1.52.0 // indirect
|
github.com/samber/lo v1.52.0 // indirect
|
||||||
github.com/samber/slog-common v0.19.0 // indirect
|
github.com/samber/slog-common v0.19.0 // indirect
|
||||||
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
|
github.com/samber/slog-zerolog/v2 v2.8.0 // indirect
|
||||||
github.com/shirou/gopsutil/v4 v4.25.9 // indirect
|
github.com/shirou/gopsutil/v4 v4.25.9 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/vincent-petithory/dataurl v1.0.0 // indirect
|
github.com/vincent-petithory/dataurl v1.0.0 // indirect
|
||||||
github.com/yusing/ds v0.2.0 // indirect
|
github.com/yusing/ds v0.3.1 // indirect
|
||||||
github.com/yusing/gointernals v0.1.16 // indirect
|
github.com/yusing/gointernals v0.1.16 // 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.63.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
golang.org/x/crypto v0.43.0 // indirect
|
golang.org/x/crypto v0.43.0 // indirect
|
||||||
|
|||||||
48
agent/go.sum
48
agent/go.sum
@@ -4,6 +4,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
|||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||||
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||||
@@ -14,8 +16,6 @@ github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7
|
|||||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
@@ -59,8 +59,8 @@ 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.26.0 h1:521aEQxNstXvPQcFDDPrJiFfixcCQuvAvm35R4GbyYA=
|
github.com/go-acme/lego/v4 v4.27.0 h1:cIhWd7Uj4BNFLEF3IpwuMkukVVRs5qjlp4KdUGa75yU=
|
||||||
github.com/go-acme/lego/v4 v4.26.0/go.mod h1:BQVAWgcyzW4IT9eIKHY/RxYlVhoyKyOMXOkq7jK1eEQ=
|
github.com/go-acme/lego/v4 v4.27.0/go.mod h1:9FfNZHZmg6hf5CWOp4Lzo4gU8aBEvqZvrwdkBboa+4g=
|
||||||
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=
|
||||||
@@ -100,12 +100,14 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
|||||||
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.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
||||||
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||||
|
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||||
|
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||||
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=
|
||||||
@@ -116,8 +118,8 @@ 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-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/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.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
|
github.com/luthermonson/go-proxmox v0.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
|
||||||
github.com/luthermonson/go-proxmox v0.2.3/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
github.com/luthermonson/go-proxmox v0.2.3/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||||
@@ -178,8 +180,8 @@ github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
|||||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||||
github.com/samber/slog-zerolog/v2 v2.7.3 h1:/MkPDl/tJhijN2GvB1MWwBn2FU8RiL3rQ8gpXkQm2EY=
|
github.com/samber/slog-zerolog/v2 v2.8.0 h1:K3+PJieRyi2rX/eaJZ95EdmpY/pzdeDd3jRnIQZG6kU=
|
||||||
github.com/samber/slog-zerolog/v2 v2.7.3/go.mod h1:oWU7WHof4Xp8VguiNO02r1a4VzkgoOyOZhY5CuRke60=
|
github.com/samber/slog-zerolog/v2 v2.8.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
@@ -201,11 +203,17 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||||
|
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||||
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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
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.2.0 h1:lPhDU5eA2uvquVrBrzLCrQXRJJgSXlUYA53TbuK2sQY=
|
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
|
||||||
github.com/yusing/ds v0.2.0/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
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=
|
||||||
@@ -216,8 +224,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG
|
|||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||||
@@ -228,8 +236,8 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
||||||
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.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
@@ -325,10 +333,10 @@ golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
|||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
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=
|
||||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
"github.com/yusing/godoxy/agent/pkg/certs"
|
"github.com/yusing/godoxy/agent/pkg/certs"
|
||||||
"github.com/yusing/goutils/version"
|
"github.com/yusing/goutils/version"
|
||||||
)
|
)
|
||||||
@@ -22,13 +23,13 @@ import (
|
|||||||
type AgentConfig struct {
|
type AgentConfig struct {
|
||||||
Addr string `json:"addr"`
|
Addr string `json:"addr"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version version.Version `json:"version"`
|
Version version.Version `json:"version" swaggertype:"string"`
|
||||||
Runtime ContainerRuntime `json:"runtime"`
|
Runtime ContainerRuntime `json:"runtime"`
|
||||||
|
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
httpClientHealthCheck *http.Client
|
fasthttpClientHealthCheck *fasthttp.Client
|
||||||
tlsConfig tls.Config
|
tlsConfig tls.Config
|
||||||
l zerolog.Logger
|
l zerolog.Logger
|
||||||
} // @name Agent
|
} // @name Agent
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -107,8 +108,7 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
|
|||||||
cfg.httpClient = cfg.NewHTTPClient()
|
cfg.httpClient = cfg.NewHTTPClient()
|
||||||
applyNormalTransportConfig(cfg.httpClient)
|
applyNormalTransportConfig(cfg.httpClient)
|
||||||
|
|
||||||
cfg.httpClientHealthCheck = cfg.NewHTTPClient()
|
cfg.fasthttpClientHealthCheck = cfg.NewFastHTTPHealthCheckClient()
|
||||||
applyHealthCheckTransportConfig(cfg.httpClientHealthCheck)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -188,6 +188,25 @@ func (cfg *AgentConfig) NewHTTPClient() *http.Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *AgentConfig) NewFastHTTPHealthCheckClient() *fasthttp.Client {
|
||||||
|
return &fasthttp.Client{
|
||||||
|
Dial: func(addr string) (net.Conn, error) {
|
||||||
|
if addr != AgentHost+":443" {
|
||||||
|
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
||||||
|
}
|
||||||
|
return net.Dial("tcp", cfg.Addr)
|
||||||
|
},
|
||||||
|
TLSConfig: &cfg.tlsConfig,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 3 * time.Second,
|
||||||
|
DisableHeaderNamesNormalizing: true,
|
||||||
|
DisablePathNormalizing: true,
|
||||||
|
NoDefaultUserAgentHeader: true,
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) Transport() *http.Transport {
|
func (cfg *AgentConfig) Transport() *http.Transport {
|
||||||
return &http.Transport{
|
return &http.Transport{
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
@@ -220,13 +239,3 @@ func applyNormalTransportConfig(client *http.Client) {
|
|||||||
transport.ReadBufferSize = 16384
|
transport.ReadBufferSize = 16384
|
||||||
transport.WriteBufferSize = 16384
|
transport.WriteBufferSize = 16384
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyHealthCheckTransportConfig(client *http.Client) {
|
|
||||||
transport := client.Transport.(*http.Transport)
|
|
||||||
transport.DisableKeepAlives = true
|
|
||||||
transport.DisableCompression = true
|
|
||||||
transport.MaxIdleConns = 1
|
|
||||||
transport.MaxIdleConnsPerHost = 1
|
|
||||||
transport.ReadBufferSize = 1024
|
|
||||||
transport.WriteBufferSize = 1024
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ package agent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
httputils "github.com/yusing/goutils/http"
|
httputils "github.com/yusing/goutils/http"
|
||||||
"github.com/yusing/goutils/http/reverseproxy"
|
"github.com/yusing/goutils/http/reverseproxy"
|
||||||
)
|
)
|
||||||
@@ -30,15 +34,44 @@ func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) (*http.Respo
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) DoHealthCheck(ctx context.Context, endpoint string) (*http.Response, error) {
|
type HealthCheckResponse struct {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", APIBaseURL+endpoint, nil)
|
Healthy bool `json:"healthy"`
|
||||||
if err != nil {
|
Detail string `json:"detail"`
|
||||||
return nil, err
|
Latency time.Duration `json:"latency"`
|
||||||
}
|
}
|
||||||
req.Header.Set("Accept-Encoding", "identity")
|
|
||||||
req.Header.Set("Connection", "close")
|
|
||||||
|
|
||||||
return cfg.httpClientHealthCheck.Do(req)
|
func (cfg *AgentConfig) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
|
||||||
|
req := fasthttp.AcquireRequest()
|
||||||
|
defer fasthttp.ReleaseRequest(req)
|
||||||
|
|
||||||
|
resp := fasthttp.AcquireResponse()
|
||||||
|
defer fasthttp.ReleaseResponse(resp)
|
||||||
|
|
||||||
|
req.SetRequestURI(APIBaseURL + EndpointHealth + "?" + query)
|
||||||
|
req.Header.SetMethod(fasthttp.MethodGet)
|
||||||
|
req.Header.Set("Accept-Encoding", "identity")
|
||||||
|
req.SetConnectionClose()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
err = cfg.fasthttpClientHealthCheck.DoTimeout(req, resp, timeout)
|
||||||
|
ret.Latency = time.Since(start)
|
||||||
|
if err != nil {
|
||||||
|
return ret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if status := resp.StatusCode(); status != http.StatusOK {
|
||||||
|
// clone body since fasthttp response will be released
|
||||||
|
body := resp.Body()
|
||||||
|
cloneBody := make([]byte, len(body))
|
||||||
|
copy(cloneBody, body)
|
||||||
|
return ret, fmt.Errorf("HTTP %d %s", status, cloneBody)
|
||||||
|
} else {
|
||||||
|
err = sonic.Unmarshal(resp.Body(), &ret)
|
||||||
|
if err != nil {
|
||||||
|
return ret, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *AgentConfig) fetchString(ctx context.Context, endpoint string) (string, int, error) {
|
func (cfg *AgentConfig) fetchString(ctx context.Context, endpoint string) (string, int, error) {
|
||||||
|
|||||||
@@ -39,5 +39,5 @@ func StartAgentServer(parent task.Parent, opt Options) {
|
|||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Start(parent, agentServer, server.WithLogger(&log.Logger))
|
server.Start(parent.Subtask("agent-server", false), agentServer, server.WithLogger(&log.Logger))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,24 +24,23 @@ services:
|
|||||||
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
|
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
|
||||||
container_name: godoxy-frontend
|
container_name: godoxy-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host # do not change this
|
|
||||||
env_file: .env
|
env_file: .env
|
||||||
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
||||||
read_only: true
|
read_only: true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /app/.next/cache # next image caching
|
- /app/.next/cache # next image caching
|
||||||
|
|
||||||
|
# for lite variant, do not change uid/gid
|
||||||
|
# - /var/cache/nginx:uid=101,gid=101
|
||||||
|
# - /run:uid=101,gid=101
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
cap_drop:
|
cap_drop:
|
||||||
- all
|
- all
|
||||||
depends_on:
|
depends_on:
|
||||||
- app
|
- app
|
||||||
environment:
|
|
||||||
HOSTNAME: 127.0.0.1
|
|
||||||
PORT: ${GODOXY_FRONTEND_PORT:-3000}
|
|
||||||
labels:
|
labels:
|
||||||
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
|
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
|
||||||
proxy.#1.port: ${GODOXY_FRONTEND_PORT:-3000}
|
|
||||||
# proxy.#1.middlewares.cidr_whitelist: |
|
# proxy.#1.middlewares.cidr_whitelist: |
|
||||||
# status: 403
|
# status: 403
|
||||||
# message: IP not allowed
|
# message: IP not allowed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
FROM alpine:3.22
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates
|
RUN apt-get update && apt-get install -y ca-certificates
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ services:
|
|||||||
API_SKIP_ORIGIN_CHECK: true
|
API_SKIP_ORIGIN_CHECK: true
|
||||||
API_JWT_TTL: 24h
|
API_JWT_TTL: 24h
|
||||||
DEBUG: true
|
DEBUG: true
|
||||||
API_SECRET: 1234567891234567
|
API_JWT_SECRET: 1234567891234567
|
||||||
labels:
|
labels:
|
||||||
proxy.exclude: true
|
proxy.exclude: true
|
||||||
proxy.#1.healthcheck.disable: true
|
proxy.#1.healthcheck.disable: true
|
||||||
@@ -42,6 +42,8 @@ services:
|
|||||||
configs:
|
configs:
|
||||||
- source: parca
|
- source: parca
|
||||||
target: /parca.yaml
|
target: /parca.yaml
|
||||||
|
labels:
|
||||||
|
proxy.#1.port: "7070"
|
||||||
tinyauth:
|
tinyauth:
|
||||||
image: ghcr.io/steveiliop56/tinyauth:v3
|
image: ghcr.io/steveiliop56/tinyauth:v3
|
||||||
container_name: tinyauth
|
container_name: tinyauth
|
||||||
|
|||||||
32
go.mod
32
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/yusing/godoxy
|
module github.com/yusing/godoxy
|
||||||
|
|
||||||
go 1.25.2
|
go 1.25.3
|
||||||
|
|
||||||
replace github.com/yusing/godoxy/agent => ./agent
|
replace github.com/yusing/godoxy/agent => ./agent
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ require (
|
|||||||
github.com/docker/docker v28.5.1+incompatible // docker daemon
|
github.com/docker/docker v28.5.1+incompatible // docker daemon
|
||||||
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.26.0 // acme client
|
github.com/go-acme/lego/v4 v4.27.0 // acme client
|
||||||
github.com/go-playground/validator/v10 v10.28.0 // validator
|
github.com/go-playground/validator/v10 v10.28.0 // 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
|
||||||
@@ -42,13 +42,13 @@ require (
|
|||||||
github.com/luthermonson/go-proxmox v0.2.3
|
github.com/luthermonson/go-proxmox v0.2.3
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1
|
github.com/oschwald/maxminddb-golang v1.13.1
|
||||||
github.com/quic-go/quic-go v0.55.0 // indirect; http3 support
|
github.com/quic-go/quic-go v0.55.0 // indirect; http3 support
|
||||||
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
|
github.com/samber/slog-zerolog/v2 v2.8.0 // indirect
|
||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/yusing/ds v0.2.0
|
github.com/yusing/ds v0.3.1
|
||||||
github.com/yusing/godoxy/agent v0.0.0-20251011032714-d1e403e16f1c
|
github.com/yusing/godoxy/agent v0.0.0-20251025144347-1ec2872f3d4c
|
||||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251011032714-d1e403e16f1c
|
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251025144347-1ec2872f3d4c
|
||||||
github.com/yusing/goutils v0.6.1
|
github.com/yusing/goutils v0.7.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -66,7 +66,6 @@ require (
|
|||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
github.com/buger/goterm v1.0.4 // indirect
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.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/diskfs/go-diskfs v1.7.0 // indirect
|
github.com/diskfs/go-diskfs v1.7.0 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
@@ -117,7 +116,7 @@ require (
|
|||||||
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.63.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||||
@@ -127,8 +126,8 @@ require (
|
|||||||
golang.org/x/sys v0.37.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
golang.org/x/text v0.30.0 // indirect
|
golang.org/x/text v0.30.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
google.golang.org/api v0.252.0 // indirect
|
google.golang.org/api v0.253.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||||
google.golang.org/grpc v1.76.0 // indirect
|
google.golang.org/grpc v1.76.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
@@ -139,11 +138,13 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.14.1
|
github.com/bytedance/sonic v1.14.1
|
||||||
github.com/shirou/gopsutil/v4 v4.25.9
|
github.com/shirou/gopsutil/v4 v4.25.9
|
||||||
|
github.com/valyala/fasthttp v1.68.0
|
||||||
github.com/yusing/gointernals v0.1.16
|
github.com/yusing/gointernals v0.1.16
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
@@ -158,15 +159,15 @@ require (
|
|||||||
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
github.com/go-resty/resty/v2 v2.16.5 // 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.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.1 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/linode/linodego v1.60.0 // indirect
|
github.com/linode/linodego v1.60.0 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
github.com/moby/term v0.5.2 // indirect
|
github.com/moby/term v0.5.2 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.0 // indirect
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.0 // indirect
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/stretchr/objx v0.5.3 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
@@ -175,6 +176,7 @@ require (
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.14 // indirect
|
github.com/ulikunitz/xz v0.5.14 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/vultr/govultr/v3 v3.24.0 // indirect
|
github.com/vultr/govultr/v3 v3.24.0 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
||||||
|
|||||||
58
go.sum
58
go.sum
@@ -37,6 +37,8 @@ github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy
|
|||||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=
|
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=
|
||||||
github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
|
github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
|
||||||
github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
|
github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
|
||||||
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||||
@@ -52,8 +54,6 @@ github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7
|
|||||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
@@ -99,8 +99,8 @@ 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.26.0 h1:521aEQxNstXvPQcFDDPrJiFfixcCQuvAvm35R4GbyYA=
|
github.com/go-acme/lego/v4 v4.27.0 h1:cIhWd7Uj4BNFLEF3IpwuMkukVVRs5qjlp4KdUGa75yU=
|
||||||
github.com/go-acme/lego/v4 v4.26.0/go.mod h1:BQVAWgcyzW4IT9eIKHY/RxYlVhoyKyOMXOkq7jK1eEQ=
|
github.com/go-acme/lego/v4 v4.27.0/go.mod h1:9FfNZHZmg6hf5CWOp4Lzo4gU8aBEvqZvrwdkBboa+4g=
|
||||||
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=
|
||||||
@@ -157,8 +157,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
|||||||
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.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
||||||
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||||
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||||
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.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||||
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=
|
||||||
@@ -193,8 +193,8 @@ github.com/linode/linodego v1.60.0 h1:SgsebJFRCi+lSmYy+C40wmKZeJllGGm+W12Qw4+yVd
|
|||||||
github.com/linode/linodego v1.60.0/go.mod h1:1+Bt0oTz5rBnDOJbGhccxn7LYVytXTIIfAy7QYmijDs=
|
github.com/linode/linodego v1.60.0/go.mod h1:1+Bt0oTz5rBnDOJbGhccxn7LYVytXTIIfAy7QYmijDs=
|
||||||
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-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/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.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
|
github.com/luthermonson/go-proxmox v0.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
|
||||||
github.com/luthermonson/go-proxmox v0.2.3/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
github.com/luthermonson/go-proxmox v0.2.3/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||||
@@ -229,10 +229,10 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
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/oci-go-sdk/common/v1065 v1065.102.0 h1:W28ZizQSS2aRWkFA3iAP9eiZS4OLFaiv35nXtq2lW/s=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1 h1:45giryNXrlUHzK/Cd4DDBOhaK0EklXrhjTgv00Zo5po=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.0/go.mod h1:cVbzGjRhtXgrduaQbR1GR1x+VDU60NcXPMZ3+eQuiiY=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.0 h1:gAOs1dkE7LFoWflzqrDqAhOprc0kF1a0fyV8C4HUPj4=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1 h1:2EthQw4pEN2rbbSLWlF9itV+Ws2xmAmIcfKYsrwCbVA=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.0/go.mod h1:EUBSYwop1K40VpcKy1haIK6kFK/gPT1atEk89OkY0Kg=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1/go.mod h1:xOLJ0zNGmF4M4LqdQclLONwdzjJewNl/7WQiZgrvYR8=
|
||||||
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=
|
||||||
@@ -275,8 +275,8 @@ github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
|||||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
|
||||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||||
github.com/samber/slog-zerolog/v2 v2.7.3 h1:/MkPDl/tJhijN2GvB1MWwBn2FU8RiL3rQ8gpXkQm2EY=
|
github.com/samber/slog-zerolog/v2 v2.8.0 h1:K3+PJieRyi2rX/eaJZ95EdmpY/pzdeDd3jRnIQZG6kU=
|
||||||
github.com/samber/slog-zerolog/v2 v2.7.3/go.mod h1:oWU7WHof4Xp8VguiNO02r1a4VzkgoOyOZhY5CuRke60=
|
github.com/samber/slog-zerolog/v2 v2.8.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
|
||||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
|
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||||
@@ -308,15 +308,21 @@ github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA
|
|||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
||||||
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||||
|
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||||
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.24.0 h1:fTTTj0VBve+Miy+wGhlb90M2NMDfpGFi6Frlj3HVy6M=
|
github.com/vultr/govultr/v3 v3.24.0 h1:fTTTj0VBve+Miy+wGhlb90M2NMDfpGFi6Frlj3HVy6M=
|
||||||
github.com/vultr/govultr/v3 v3.24.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
github.com/vultr/govultr/v3 v3.24.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
github.com/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.2.0 h1:lPhDU5eA2uvquVrBrzLCrQXRJJgSXlUYA53TbuK2sQY=
|
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
|
||||||
github.com/yusing/ds v0.2.0/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
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=
|
||||||
@@ -329,8 +335,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG
|
|||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||||
@@ -341,8 +347,8 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
||||||
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.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
@@ -443,14 +449,14 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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.252.0 h1:xfKJeAJaMwb8OC9fesr369rjciQ704AjU/psjkKURSI=
|
google.golang.org/api v0.253.0 h1:apU86Eq9Q2eQco3NsUYFpVTfy7DwemojL7LmbAj7g/I=
|
||||||
google.golang.org/api v0.252.0/go.mod h1:dnHOv81x5RAmumZ7BWLShB/u7JZNeyalImxHmtTHxqw=
|
google.golang.org/api v0.253.0/go.mod h1:PX09ad0r/4du83vZVAaGg7OaeyGnaUmT/CYPNvtLCbw=
|
||||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
||||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk=
|
google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
2
goutils
2
goutils
Submodule goutils updated: 26146bd560...c0955732e9
@@ -2,13 +2,12 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"reflect"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gin-gonic/gin/codec/json"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
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"
|
||||||
authApi "github.com/yusing/godoxy/internal/api/v1/auth"
|
authApi "github.com/yusing/godoxy/internal/api/v1/auth"
|
||||||
@@ -20,6 +19,7 @@ import (
|
|||||||
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
|
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
|
||||||
"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"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,6 +45,9 @@ func NewHandler() *gin.Engine {
|
|||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(ErrorHandler())
|
r.Use(ErrorHandler())
|
||||||
r.Use(ErrorLoggingMiddleware())
|
r.Use(ErrorLoggingMiddleware())
|
||||||
|
r.Use(NoCache())
|
||||||
|
|
||||||
|
log.Debug().Msg("gin codec json.API: " + reflect.TypeOf(json.API).Name())
|
||||||
|
|
||||||
r.GET("/api/v1/version", apiV1.Version)
|
r.GET("/api/v1/version", apiV1.Version)
|
||||||
|
|
||||||
@@ -69,7 +72,7 @@ func NewHandler() *gin.Engine {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
// enable cache for favicon
|
// enable cache for favicon
|
||||||
v1.GET("/favicon", apiV1.FavIcon).Use(Cache(time.Hour * 24))
|
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.POST("/reload", apiV1.Reload)
|
||||||
@@ -81,6 +84,7 @@ func NewHandler() *gin.Engine {
|
|||||||
route.GET("/:which", routeApi.Route)
|
route.GET("/:which", routeApi.Route)
|
||||||
route.GET("/providers", routeApi.Providers)
|
route.GET("/providers", routeApi.Providers)
|
||||||
route.GET("/by_provider", routeApi.ByProvider)
|
route.GET("/by_provider", routeApi.ByProvider)
|
||||||
|
route.POST("/playground", routeApi.Playground)
|
||||||
}
|
}
|
||||||
|
|
||||||
file := v1.Group("/file")
|
file := v1.Group("/file")
|
||||||
@@ -139,15 +143,13 @@ func NewHandler() *gin.Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// disable cache by default
|
|
||||||
r.Use(NoCache())
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func NoCache() gin.HandlerFunc {
|
func NoCache() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// skip cache if Cache-Control header is set or if caching is explicitly enabled
|
// skip cache if Cache-Control header is set
|
||||||
if !c.GetBool("cache_enabled") && c.Writer.Header().Get("Cache-Control") == "" {
|
if c.Writer.Header().Get("Cache-Control") == "" {
|
||||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
c.Header("Pragma", "no-cache")
|
c.Header("Pragma", "no-cache")
|
||||||
c.Header("Expires", "0")
|
c.Header("Expires", "0")
|
||||||
@@ -156,20 +158,6 @@ func NoCache() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Cache(duration time.Duration) gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
// Signal to NoCache middleware that caching is intended
|
|
||||||
c.Set("cache_enabled", true)
|
|
||||||
// skip cache if Cache-Control header is set
|
|
||||||
if c.Writer.Header().Get("Cache-Control") == "" {
|
|
||||||
c.Header("Cache-Control", "public, max-age="+strconv.FormatFloat(duration.Seconds(), 'f', 0, 64)+", immutable")
|
|
||||||
c.Header("Pragma", "public")
|
|
||||||
c.Header("Expires", time.Now().Add(duration).Format(time.RFC1123))
|
|
||||||
}
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AuthMiddleware() gin.HandlerFunc {
|
func AuthMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
err := auth.GetDefaultAuth().CheckToken(c.Request)
|
err := auth.GetDefaultAuth().CheckToken(c.Request)
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
package apitypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
gperr "github.com/yusing/goutils/errs"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ErrorResponse struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Error string `json:"error,omitempty" extensions:"x-nullable"`
|
|
||||||
} // @name ErrorResponse
|
|
||||||
|
|
||||||
type serverError struct {
|
|
||||||
Message string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns a generic error response
|
|
||||||
func Error(message string, err ...error) ErrorResponse {
|
|
||||||
if len(err) > 0 {
|
|
||||||
var gpErr gperr.Error
|
|
||||||
if errors.As(err[0], &gpErr) {
|
|
||||||
return ErrorResponse{
|
|
||||||
Message: message,
|
|
||||||
Error: string(gpErr.Plain()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ErrorResponse{
|
|
||||||
Message: message,
|
|
||||||
Error: err[0].Error(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ErrorResponse{
|
|
||||||
Message: message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func InternalServerError(err error, message string) error {
|
|
||||||
return serverError{
|
|
||||||
Message: message,
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e serverError) Error() string {
|
|
||||||
if e.Err != nil {
|
|
||||||
return e.Message + ": " + e.Err.Error()
|
|
||||||
}
|
|
||||||
return e.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e serverError) Unwrap() error {
|
|
||||||
return e.Err
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package apitypes
|
|
||||||
|
|
||||||
type QueryOptions struct {
|
|
||||||
Limit int `binding:"required,min=1,max=20" form:"limit"`
|
|
||||||
Offset int `binding:"omitempty,min=0" form:"offset"`
|
|
||||||
OrderBy QueryOrder `binding:"omitempty,oneof=created_at updated_at" form:"order_by"`
|
|
||||||
Order QueryOrderDirection `binding:"omitempty,oneof=asc desc" form:"order"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type QueryOrder string
|
|
||||||
|
|
||||||
const (
|
|
||||||
QueryOrderCreatedAt QueryOrder = "created_at"
|
|
||||||
QueryOrderUpdatedAt QueryOrder = "updated_at"
|
|
||||||
)
|
|
||||||
|
|
||||||
type QueryOrderDirection string
|
|
||||||
|
|
||||||
const (
|
|
||||||
QueryOrderDirectionAsc QueryOrderDirection = "asc"
|
|
||||||
QueryOrderDirectionDesc QueryOrderDirection = "desc"
|
|
||||||
)
|
|
||||||
|
|
||||||
type QueryResponse struct {
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
Offset int `json:"offset"`
|
|
||||||
HasMore bool `json:"has_more"`
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package apitypes
|
|
||||||
|
|
||||||
type SuccessResponse struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Details map[string]any `json:"details,omitempty" extensions:"x-nullable"`
|
|
||||||
} // @name SuccessResponse
|
|
||||||
|
|
||||||
func Success(message string, extra ...map[string]any) SuccessResponse {
|
|
||||||
if len(extra) > 0 {
|
|
||||||
return SuccessResponse{
|
|
||||||
Message: message,
|
|
||||||
Details: extra[0],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return SuccessResponse{
|
|
||||||
Message: message,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
|
||||||
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "list"
|
// @x-id "list"
|
||||||
@@ -19,7 +21,6 @@ import (
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} Agent
|
// @Success 200 {array} Agent
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
|
||||||
// @Router /agent/list [get]
|
// @Router /agent/list [get]
|
||||||
func List(c *gin.Context) {
|
func List(c *gin.Context) {
|
||||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CertInfo struct {
|
type CertInfo struct {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"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"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "container"
|
// @x-id "container"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
|
||||||
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContainerState = container.ContainerState // @name ContainerState
|
type ContainerState = container.ContainerState // @name ContainerState
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
|
|
||||||
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type containerStats struct {
|
type containerStats struct {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"github.com/docker/docker/pkg/stdcopy"
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "restart"
|
// @x-id "restart"
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StartRequest struct {
|
type StartRequest struct {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StopRequest struct {
|
type StopRequest struct {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
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"
|
||||||
|
|||||||
@@ -105,12 +105,6 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/ErrorResponse"
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-id": "list",
|
"x-id": "list",
|
||||||
@@ -2135,6 +2129,54 @@
|
|||||||
"operationId": "routes"
|
"operationId": "routes"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/route/playground": {
|
||||||
|
"post": {
|
||||||
|
"description": "Test rules against mock request/response",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"summary": "Rule Playground",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Playground request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/PlaygroundRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/PlaygroundResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-id": "playground",
|
||||||
|
"operationId": "playground"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/route/providers": {
|
"/route/providers": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List route providers",
|
"description": "List route providers",
|
||||||
@@ -2726,6 +2768,83 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
|
"FinalRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"body": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"host": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"FinalResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"body": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"statusCode": {
|
||||||
|
"type": "integer",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
"HTTPHeader": {
|
"HTTPHeader": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2799,6 +2918,75 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
|
"HealthInfo": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"latency": {
|
||||||
|
"description": "latency in microseconds",
|
||||||
|
"type": "number",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"healthy",
|
||||||
|
"unhealthy",
|
||||||
|
"napping",
|
||||||
|
"starting",
|
||||||
|
"error",
|
||||||
|
"unknown"
|
||||||
|
],
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"uptime": {
|
||||||
|
"description": "uptime in milliseconds",
|
||||||
|
"type": "number",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"HealthInfoWithoutDetail": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"latency": {
|
||||||
|
"description": "latency in microseconds",
|
||||||
|
"type": "number",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"healthy",
|
||||||
|
"unhealthy",
|
||||||
|
"napping",
|
||||||
|
"starting",
|
||||||
|
"error",
|
||||||
|
"unknown"
|
||||||
|
],
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"uptime": {
|
||||||
|
"description": "uptime in milliseconds",
|
||||||
|
"type": "number",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
"HealthJSON": {
|
"HealthJSON": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2882,7 +3070,7 @@
|
|||||||
"HealthMap": {
|
"HealthMap": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/definitions/routes.HealthInfo"
|
"$ref": "#/definitions/HealthInfo"
|
||||||
},
|
},
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
@@ -3494,6 +3682,113 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
|
"MockCookie": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"MockRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"body": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"cookies": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/MockCookie"
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"host": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"remoteIP": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"MockResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"body": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"statusCode": {
|
||||||
|
"type": "integer",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
"NewAgentRequest": {
|
"NewAgentRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -3589,6 +3884,120 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
|
"ParsedRule": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"do": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"isResponseRule": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"on": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"validationError": {
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"PlaygroundRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"rules"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"mockRequest": {
|
||||||
|
"$ref": "#/definitions/MockRequest"
|
||||||
|
},
|
||||||
|
"mockResponse": {
|
||||||
|
"$ref": "#/definitions/MockResponse"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/routeApi.RawRule"
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"PlaygroundResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"executionError": {
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"finalRequest": {
|
||||||
|
"$ref": "#/definitions/FinalRequest",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"finalResponse": {
|
||||||
|
"$ref": "#/definitions/FinalResponse",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"matchedRules": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"parsedRules": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/ParsedRule"
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"upstreamCalled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"Port": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"listening": {
|
||||||
|
"type": "integer",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"proxy": {
|
||||||
|
"type": "integer",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
"ProviderStats": {
|
"ProviderStats": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -3844,7 +4253,7 @@
|
|||||||
"x-nullable": true
|
"x-nullable": true
|
||||||
},
|
},
|
||||||
"port": {
|
"port": {
|
||||||
"$ref": "#/definitions/github_com_yusing_go-proxy_internal_route_types.Port",
|
"$ref": "#/definitions/Port",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
@@ -3868,9 +4277,12 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
|
"rule_file": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": true
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"uniqueItems": true,
|
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/rules.Rule"
|
"$ref": "#/definitions/rules.Rule"
|
||||||
},
|
},
|
||||||
@@ -3878,7 +4290,47 @@
|
|||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"scheme": {
|
"scheme": {
|
||||||
"$ref": "#/definitions/route.Scheme",
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"tcp",
|
||||||
|
"udp",
|
||||||
|
"fileserver"
|
||||||
|
],
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"ssl_certificate": {
|
||||||
|
"description": "Path to client certificate",
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"ssl_certificate_key": {
|
||||||
|
"description": "Path to client certificate key",
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"ssl_protocols": {
|
||||||
|
"description": "Allowed TLS protocols",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"ssl_server_name": {
|
||||||
|
"description": "SSL/TLS proxy options (nginx-like)",
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"ssl_trusted_certificate": {
|
||||||
|
"description": "Path to trusted CA certificates",
|
||||||
|
"type": "string",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
}
|
}
|
||||||
@@ -3975,7 +4427,7 @@
|
|||||||
"statuses": {
|
"statuses": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/definitions/routes.HealthInfo"
|
"$ref": "#/definitions/HealthInfoWithoutDetail"
|
||||||
},
|
},
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
@@ -4499,7 +4951,6 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"iops": {
|
"iops": {
|
||||||
"description": "godoxy",
|
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
@@ -4522,7 +4973,6 @@
|
|||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"read_speed": {
|
"read_speed": {
|
||||||
"description": "godoxy",
|
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
@@ -4538,7 +4988,6 @@
|
|||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"write_speed": {
|
"write_speed": {
|
||||||
"description": "godoxy",
|
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
@@ -4566,7 +5015,7 @@
|
|||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"total": {
|
"total": {
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
@@ -4628,31 +5077,9 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"github_com_yusing_go-proxy_internal_route_types.Port": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"listening": {
|
|
||||||
"type": "integer",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"proxy": {
|
|
||||||
"type": "integer",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"homepage.FetchResult": {
|
"homepage.FetchResult": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"errMsg": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"icon": {
|
"icon": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -4739,15 +5166,9 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"free": {
|
|
||||||
"description": "This is the kernel's notion of free memory; RAM chips whose bits nobody\ncares about the value of right now. For a human consumable number,\nAvailable is what you really want.",
|
|
||||||
"type": "integer",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"total": {
|
"total": {
|
||||||
"description": "Total amount of RAM on this system",
|
"description": "Total amount of RAM on this system",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
@@ -4907,7 +5328,7 @@
|
|||||||
"x-nullable": true
|
"x-nullable": true
|
||||||
},
|
},
|
||||||
"port": {
|
"port": {
|
||||||
"$ref": "#/definitions/github_com_yusing_go-proxy_internal_route_types.Port",
|
"$ref": "#/definitions/Port",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
@@ -4931,9 +5352,12 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
|
"rule_file": {
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": true
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"uniqueItems": true,
|
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/rules.Rule"
|
"$ref": "#/definitions/rules.Rule"
|
||||||
},
|
},
|
||||||
@@ -4941,7 +5365,47 @@
|
|||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"scheme": {
|
"scheme": {
|
||||||
"$ref": "#/definitions/route.Scheme",
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"tcp",
|
||||||
|
"udp",
|
||||||
|
"fileserver"
|
||||||
|
],
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"ssl_certificate": {
|
||||||
|
"description": "Path to client certificate",
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"ssl_certificate_key": {
|
||||||
|
"description": "Path to client certificate key",
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"ssl_protocols": {
|
||||||
|
"description": "Allowed TLS protocols",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"ssl_server_name": {
|
||||||
|
"description": "SSL/TLS proxy options (nginx-like)",
|
||||||
|
"type": "string",
|
||||||
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
},
|
||||||
|
"ssl_trusted_certificate": {
|
||||||
|
"description": "Path to trusted CA certificates",
|
||||||
|
"type": "string",
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
}
|
}
|
||||||
@@ -4949,22 +5413,25 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"route.Scheme": {
|
"routeApi.RawRule": {
|
||||||
"type": "string",
|
"type": "object",
|
||||||
"enum": [
|
"properties": {
|
||||||
"http",
|
"do": {
|
||||||
"https",
|
"type": "string",
|
||||||
"tcp",
|
"x-nullable": false,
|
||||||
"udp",
|
"x-omitempty": false
|
||||||
"fileserver"
|
},
|
||||||
],
|
"name": {
|
||||||
"x-enum-varnames": [
|
"type": "string",
|
||||||
"SchemeHTTP",
|
"x-nullable": false,
|
||||||
"SchemeHTTPS",
|
"x-omitempty": false
|
||||||
"SchemeTCP",
|
},
|
||||||
"SchemeUDP",
|
"on": {
|
||||||
"SchemeFileServer"
|
"type": "string",
|
||||||
],
|
"x-nullable": false,
|
||||||
|
"x-omitempty": false
|
||||||
|
}
|
||||||
|
},
|
||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
@@ -4979,43 +5446,6 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"routes.HealthInfo": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"detail": {
|
|
||||||
"type": "string",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"latency": {
|
|
||||||
"description": "latency in microseconds",
|
|
||||||
"type": "number",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"healthy",
|
|
||||||
"unhealthy",
|
|
||||||
"napping",
|
|
||||||
"starting",
|
|
||||||
"error",
|
|
||||||
"unknown"
|
|
||||||
],
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"uptime": {
|
|
||||||
"description": "uptime in milliseconds",
|
|
||||||
"type": "number",
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"rules.Rule": {
|
"rules.Rule": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -217,6 +217,42 @@ definitions:
|
|||||||
- FileTypeConfig
|
- FileTypeConfig
|
||||||
- FileTypeProvider
|
- FileTypeProvider
|
||||||
- FileTypeMiddleware
|
- FileTypeMiddleware
|
||||||
|
FinalRequest:
|
||||||
|
properties:
|
||||||
|
body:
|
||||||
|
type: string
|
||||||
|
headers:
|
||||||
|
additionalProperties:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
host:
|
||||||
|
type: string
|
||||||
|
method:
|
||||||
|
type: string
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
query:
|
||||||
|
additionalProperties:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
FinalResponse:
|
||||||
|
properties:
|
||||||
|
body:
|
||||||
|
type: string
|
||||||
|
headers:
|
||||||
|
additionalProperties:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
statusCode:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
HTTPHeader:
|
HTTPHeader:
|
||||||
properties:
|
properties:
|
||||||
key:
|
key:
|
||||||
@@ -248,6 +284,44 @@ definitions:
|
|||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
|
HealthInfo:
|
||||||
|
properties:
|
||||||
|
detail:
|
||||||
|
type: string
|
||||||
|
latency:
|
||||||
|
description: latency in microseconds
|
||||||
|
type: number
|
||||||
|
status:
|
||||||
|
enum:
|
||||||
|
- healthy
|
||||||
|
- unhealthy
|
||||||
|
- napping
|
||||||
|
- starting
|
||||||
|
- error
|
||||||
|
- unknown
|
||||||
|
type: string
|
||||||
|
uptime:
|
||||||
|
description: uptime in milliseconds
|
||||||
|
type: number
|
||||||
|
type: object
|
||||||
|
HealthInfoWithoutDetail:
|
||||||
|
properties:
|
||||||
|
latency:
|
||||||
|
description: latency in microseconds
|
||||||
|
type: number
|
||||||
|
status:
|
||||||
|
enum:
|
||||||
|
- healthy
|
||||||
|
- unhealthy
|
||||||
|
- napping
|
||||||
|
- starting
|
||||||
|
- error
|
||||||
|
- unknown
|
||||||
|
type: string
|
||||||
|
uptime:
|
||||||
|
description: uptime in milliseconds
|
||||||
|
type: number
|
||||||
|
type: object
|
||||||
HealthJSON:
|
HealthJSON:
|
||||||
properties:
|
properties:
|
||||||
config:
|
config:
|
||||||
@@ -283,7 +357,7 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
HealthMap:
|
HealthMap:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
$ref: '#/definitions/routes.HealthInfo'
|
$ref: '#/definitions/HealthInfo'
|
||||||
type: object
|
type: object
|
||||||
HomepageCategory:
|
HomepageCategory:
|
||||||
properties:
|
properties:
|
||||||
@@ -564,6 +638,55 @@ definitions:
|
|||||||
- MetricsPeriod1h
|
- MetricsPeriod1h
|
||||||
- MetricsPeriod1d
|
- MetricsPeriod1d
|
||||||
- MetricsPeriod1mo
|
- MetricsPeriod1mo
|
||||||
|
MockCookie:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
MockRequest:
|
||||||
|
properties:
|
||||||
|
body:
|
||||||
|
type: string
|
||||||
|
cookies:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/MockCookie'
|
||||||
|
type: array
|
||||||
|
headers:
|
||||||
|
additionalProperties:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
host:
|
||||||
|
type: string
|
||||||
|
method:
|
||||||
|
type: string
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
query:
|
||||||
|
additionalProperties:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
remoteIP:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
MockResponse:
|
||||||
|
properties:
|
||||||
|
body:
|
||||||
|
type: string
|
||||||
|
headers:
|
||||||
|
additionalProperties:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
statusCode:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
NewAgentRequest:
|
NewAgentRequest:
|
||||||
properties:
|
properties:
|
||||||
container_runtime:
|
container_runtime:
|
||||||
@@ -612,6 +735,56 @@ definitions:
|
|||||||
format: base64
|
format: base64
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
ParsedRule:
|
||||||
|
properties:
|
||||||
|
do:
|
||||||
|
type: string
|
||||||
|
isResponseRule:
|
||||||
|
type: boolean
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
"on":
|
||||||
|
type: string
|
||||||
|
validationError: {}
|
||||||
|
type: object
|
||||||
|
PlaygroundRequest:
|
||||||
|
properties:
|
||||||
|
mockRequest:
|
||||||
|
$ref: '#/definitions/MockRequest'
|
||||||
|
mockResponse:
|
||||||
|
$ref: '#/definitions/MockResponse'
|
||||||
|
rules:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/routeApi.RawRule'
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- rules
|
||||||
|
type: object
|
||||||
|
PlaygroundResponse:
|
||||||
|
properties:
|
||||||
|
executionError: {}
|
||||||
|
finalRequest:
|
||||||
|
$ref: '#/definitions/FinalRequest'
|
||||||
|
finalResponse:
|
||||||
|
$ref: '#/definitions/FinalResponse'
|
||||||
|
matchedRules:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
parsedRules:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/ParsedRule'
|
||||||
|
type: array
|
||||||
|
upstreamCalled:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
Port:
|
||||||
|
properties:
|
||||||
|
listening:
|
||||||
|
type: integer
|
||||||
|
proxy:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
ProviderStats:
|
ProviderStats:
|
||||||
properties:
|
properties:
|
||||||
reverse_proxies:
|
reverse_proxies:
|
||||||
@@ -738,7 +911,7 @@ definitions:
|
|||||||
type: array
|
type: array
|
||||||
x-nullable: true
|
x-nullable: true
|
||||||
port:
|
port:
|
||||||
$ref: '#/definitions/github_com_yusing_go-proxy_internal_route_types.Port'
|
$ref: '#/definitions/Port'
|
||||||
provider:
|
provider:
|
||||||
description: for backward compatibility
|
description: for backward compatibility
|
||||||
type: string
|
type: string
|
||||||
@@ -749,13 +922,38 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
root:
|
root:
|
||||||
type: string
|
type: string
|
||||||
|
rule_file:
|
||||||
|
type: string
|
||||||
|
x-nullable: true
|
||||||
rules:
|
rules:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/rules.Rule'
|
$ref: '#/definitions/rules.Rule'
|
||||||
type: array
|
type: array
|
||||||
uniqueItems: true
|
|
||||||
scheme:
|
scheme:
|
||||||
$ref: '#/definitions/route.Scheme'
|
enum:
|
||||||
|
- http
|
||||||
|
- https
|
||||||
|
- tcp
|
||||||
|
- udp
|
||||||
|
- fileserver
|
||||||
|
type: string
|
||||||
|
ssl_certificate:
|
||||||
|
description: Path to client certificate
|
||||||
|
type: string
|
||||||
|
ssl_certificate_key:
|
||||||
|
description: Path to client certificate key
|
||||||
|
type: string
|
||||||
|
ssl_protocols:
|
||||||
|
description: Allowed TLS protocols
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
ssl_server_name:
|
||||||
|
description: SSL/TLS proxy options (nginx-like)
|
||||||
|
type: string
|
||||||
|
ssl_trusted_certificate:
|
||||||
|
description: Path to trusted CA certificates
|
||||||
|
type: string
|
||||||
type: object
|
type: object
|
||||||
RouteProvider:
|
RouteProvider:
|
||||||
properties:
|
properties:
|
||||||
@@ -798,7 +996,7 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
statuses:
|
statuses:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
$ref: '#/definitions/routes.HealthInfo'
|
$ref: '#/definitions/HealthInfoWithoutDetail'
|
||||||
type: object
|
type: object
|
||||||
timestamp:
|
timestamp:
|
||||||
type: integer
|
type: integer
|
||||||
@@ -1072,7 +1270,6 @@ definitions:
|
|||||||
disk.IOCountersStat:
|
disk.IOCountersStat:
|
||||||
properties:
|
properties:
|
||||||
iops:
|
iops:
|
||||||
description: godoxy
|
|
||||||
type: integer
|
type: integer
|
||||||
name:
|
name:
|
||||||
description: |-
|
description: |-
|
||||||
@@ -1096,14 +1293,12 @@ definitions:
|
|||||||
read_count:
|
read_count:
|
||||||
type: integer
|
type: integer
|
||||||
read_speed:
|
read_speed:
|
||||||
description: godoxy
|
|
||||||
type: number
|
type: number
|
||||||
write_bytes:
|
write_bytes:
|
||||||
type: integer
|
type: integer
|
||||||
write_count:
|
write_count:
|
||||||
type: integer
|
type: integer
|
||||||
write_speed:
|
write_speed:
|
||||||
description: godoxy
|
|
||||||
type: number
|
type: number
|
||||||
type: object
|
type: object
|
||||||
disk.UsageStat:
|
disk.UsageStat:
|
||||||
@@ -1115,7 +1310,7 @@ definitions:
|
|||||||
path:
|
path:
|
||||||
type: string
|
type: string
|
||||||
total:
|
total:
|
||||||
type: integer
|
type: number
|
||||||
used:
|
used:
|
||||||
type: integer
|
type: integer
|
||||||
used_percent:
|
used_percent:
|
||||||
@@ -1156,17 +1351,8 @@ definitions:
|
|||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
type: object
|
type: object
|
||||||
github_com_yusing_go-proxy_internal_route_types.Port:
|
|
||||||
properties:
|
|
||||||
listening:
|
|
||||||
type: integer
|
|
||||||
proxy:
|
|
||||||
type: integer
|
|
||||||
type: object
|
|
||||||
homepage.FetchResult:
|
homepage.FetchResult:
|
||||||
properties:
|
properties:
|
||||||
errMsg:
|
|
||||||
type: string
|
|
||||||
icon:
|
icon:
|
||||||
items:
|
items:
|
||||||
format: int32
|
format: int32
|
||||||
@@ -1212,15 +1398,9 @@ definitions:
|
|||||||
|
|
||||||
This value is computed from the kernel specific values.
|
This value is computed from the kernel specific values.
|
||||||
type: integer
|
type: integer
|
||||||
free:
|
|
||||||
description: |-
|
|
||||||
This is the kernel's notion of free memory; RAM chips whose bits nobody
|
|
||||||
cares about the value of right now. For a human consumable number,
|
|
||||||
Available is what you really want.
|
|
||||||
type: integer
|
|
||||||
total:
|
total:
|
||||||
description: Total amount of RAM on this system
|
description: Total amount of RAM on this system
|
||||||
type: integer
|
type: number
|
||||||
used:
|
used:
|
||||||
description: |-
|
description: |-
|
||||||
RAM used by programs
|
RAM used by programs
|
||||||
@@ -1307,7 +1487,7 @@ definitions:
|
|||||||
type: array
|
type: array
|
||||||
x-nullable: true
|
x-nullable: true
|
||||||
port:
|
port:
|
||||||
$ref: '#/definitions/github_com_yusing_go-proxy_internal_route_types.Port'
|
$ref: '#/definitions/Port'
|
||||||
provider:
|
provider:
|
||||||
description: for backward compatibility
|
description: for backward compatibility
|
||||||
type: string
|
type: string
|
||||||
@@ -1318,54 +1498,54 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
root:
|
root:
|
||||||
type: string
|
type: string
|
||||||
|
rule_file:
|
||||||
|
type: string
|
||||||
|
x-nullable: true
|
||||||
rules:
|
rules:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/rules.Rule'
|
$ref: '#/definitions/rules.Rule'
|
||||||
type: array
|
type: array
|
||||||
uniqueItems: true
|
|
||||||
scheme:
|
scheme:
|
||||||
$ref: '#/definitions/route.Scheme'
|
enum:
|
||||||
|
- http
|
||||||
|
- https
|
||||||
|
- tcp
|
||||||
|
- udp
|
||||||
|
- fileserver
|
||||||
|
type: string
|
||||||
|
ssl_certificate:
|
||||||
|
description: Path to client certificate
|
||||||
|
type: string
|
||||||
|
ssl_certificate_key:
|
||||||
|
description: Path to client certificate key
|
||||||
|
type: string
|
||||||
|
ssl_protocols:
|
||||||
|
description: Allowed TLS protocols
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
ssl_server_name:
|
||||||
|
description: SSL/TLS proxy options (nginx-like)
|
||||||
|
type: string
|
||||||
|
ssl_trusted_certificate:
|
||||||
|
description: Path to trusted CA certificates
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
routeApi.RawRule:
|
||||||
|
properties:
|
||||||
|
do:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
"on":
|
||||||
|
type: string
|
||||||
type: object
|
type: object
|
||||||
route.Scheme:
|
|
||||||
enum:
|
|
||||||
- http
|
|
||||||
- https
|
|
||||||
- tcp
|
|
||||||
- udp
|
|
||||||
- fileserver
|
|
||||||
type: string
|
|
||||||
x-enum-varnames:
|
|
||||||
- SchemeHTTP
|
|
||||||
- SchemeHTTPS
|
|
||||||
- SchemeTCP
|
|
||||||
- SchemeUDP
|
|
||||||
- SchemeFileServer
|
|
||||||
routeApi.RoutesByProvider:
|
routeApi.RoutesByProvider:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/route.Route'
|
$ref: '#/definitions/route.Route'
|
||||||
type: array
|
type: array
|
||||||
type: object
|
type: object
|
||||||
routes.HealthInfo:
|
|
||||||
properties:
|
|
||||||
detail:
|
|
||||||
type: string
|
|
||||||
latency:
|
|
||||||
description: latency in microseconds
|
|
||||||
type: number
|
|
||||||
status:
|
|
||||||
enum:
|
|
||||||
- healthy
|
|
||||||
- unhealthy
|
|
||||||
- napping
|
|
||||||
- starting
|
|
||||||
- error
|
|
||||||
- unknown
|
|
||||||
type: string
|
|
||||||
uptime:
|
|
||||||
description: uptime in milliseconds
|
|
||||||
type: number
|
|
||||||
type: object
|
|
||||||
rules.Rule:
|
rules.Rule:
|
||||||
properties:
|
properties:
|
||||||
do:
|
do:
|
||||||
@@ -1494,10 +1674,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 agents
|
summary: List agents
|
||||||
tags:
|
tags:
|
||||||
- agent
|
- agent
|
||||||
@@ -2878,6 +3054,37 @@ paths:
|
|||||||
- route
|
- route
|
||||||
- websocket
|
- websocket
|
||||||
x-id: routes
|
x-id: routes
|
||||||
|
/route/playground:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Test rules against mock request/response
|
||||||
|
parameters:
|
||||||
|
- description: Playground request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/PlaygroundRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/PlaygroundResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
summary: Rule Playground
|
||||||
|
tags:
|
||||||
|
- route
|
||||||
|
x-id: playground
|
||||||
/route/providers:
|
/route/providers:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
|
||||||
_ "unsafe"
|
_ "unsafe"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileType string // @name FileType
|
type FileType string // @name FileType
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
"github.com/yusing/godoxy/internal/utils"
|
"github.com/yusing/godoxy/internal/utils"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListFilesResponse struct {
|
type ListFilesResponse struct {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SetFileContentRequest GetFileContentRequest
|
type SetFileContentRequest GetFileContentRequest
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||||
"github.com/yusing/godoxy/internal/route/provider"
|
"github.com/yusing/godoxy/internal/route/provider"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
|
||||||
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HealthMap = map[string]routes.HealthInfo // @name HealthMap
|
type HealthMap = map[string]routes.HealthInfo // @name HealthMap
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
|
|
||||||
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "categories"
|
// @x-id "categories"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HomepageOverrideItemClickParams struct {
|
type HomepageOverrideItemClickParams struct {
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
|
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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListIconsRequest struct {
|
type ListIconsRequest struct {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package metrics
|
package metrics
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -14,21 +12,17 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/metrics/period"
|
"github.com/yusing/godoxy/internal/metrics/period"
|
||||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
"github.com/yusing/goutils/synk"
|
"github.com/yusing/goutils/synk"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var bytesPool = synk.GetUnsizedBytesPool()
|
||||||
// for json marshaling (unknown size)
|
|
||||||
allSystemInfoBytesPool = synk.GetBytesPoolWithUniqueMemory()
|
|
||||||
// for storing http response body (known size)
|
|
||||||
allSystemInfoFixedSizePool = synk.GetBytesPool()
|
|
||||||
)
|
|
||||||
|
|
||||||
type AllSystemInfoRequest struct {
|
type AllSystemInfoRequest struct {
|
||||||
Period period.Filter `query:"period"`
|
Period period.Filter `query:"period"`
|
||||||
@@ -38,6 +32,7 @@ type AllSystemInfoRequest struct {
|
|||||||
|
|
||||||
type bytesFromPool struct {
|
type bytesFromPool struct {
|
||||||
json.RawMessage
|
json.RawMessage
|
||||||
|
release func([]byte)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @x-id "all_system_info"
|
// @x-id "all_system_info"
|
||||||
@@ -183,38 +178,26 @@ func AllSystemInfo(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAgentSystemInfo(ctx context.Context, a *agent.AgentConfig, query string) (json.Marshaler, error) {
|
func getAgentSystemInfo(ctx context.Context, a *agent.AgentConfig, query string) (bytesFromPool, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
path := agent.EndpointSystemInfo + "?" + query
|
path := agent.EndpointSystemInfo + "?" + query
|
||||||
resp, err := a.Do(ctx, http.MethodGet, path, nil)
|
resp, err := a.Do(ctx, http.MethodGet, path, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return bytesFromPool{}, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// NOTE: buffer will be released by marshalSystemInfo once marshaling is done.
|
// NOTE: buffer will be released by marshalSystemInfo once marshaling is done.
|
||||||
if resp.ContentLength >= 0 {
|
bytesBuf, release, err := httputils.ReadAllBody(resp)
|
||||||
bytesBuf := allSystemInfoFixedSizePool.GetSized(int(resp.ContentLength))
|
|
||||||
_, err = io.ReadFull(resp.Body, bytesBuf)
|
|
||||||
if err != nil {
|
|
||||||
// prevent pool leak on error.
|
|
||||||
allSystemInfoFixedSizePool.Put(bytesBuf)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return bytesFromPool{json.RawMessage(bytesBuf)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback when content length is unknown (should not happen but just in case).
|
|
||||||
data, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return bytesFromPool{}, err
|
||||||
}
|
}
|
||||||
return json.RawMessage(data), nil
|
return bytesFromPool{json.RawMessage(bytesBuf), release}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAgentSystemInfoWithRetry(ctx context.Context, a *agent.AgentConfig, query string) (json.Marshaler, error) {
|
func getAgentSystemInfoWithRetry(ctx context.Context, a *agent.AgentConfig, query string) (bytesFromPool, error) {
|
||||||
const maxRetries = 3
|
const maxRetries = 3
|
||||||
var lastErr error
|
var lastErr error
|
||||||
|
|
||||||
@@ -224,7 +207,7 @@ func getAgentSystemInfoWithRetry(ctx context.Context, a *agent.AgentConfig, quer
|
|||||||
delay := max((1<<attempt)*time.Second, 5*time.Second)
|
delay := max((1<<attempt)*time.Second, 5*time.Second)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
return bytesFromPool{}, ctx.Err()
|
||||||
case <-time.After(delay):
|
case <-time.After(delay):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,23 +223,22 @@ func getAgentSystemInfoWithRetry(ctx context.Context, a *agent.AgentConfig, quer
|
|||||||
|
|
||||||
// Don't retry on context cancellation
|
// Don't retry on context cancellation
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return nil, ctx.Err()
|
return bytesFromPool{}, ctx.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, lastErr
|
return bytesFromPool{}, lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any) error {
|
func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any) error {
|
||||||
bytesBuf := allSystemInfoBytesPool.Get()
|
buf := bytesPool.GetBuffer()
|
||||||
defer allSystemInfoBytesPool.Put(bytesBuf)
|
defer bytesPool.PutBuffer(buf)
|
||||||
|
|
||||||
// release the buffer retrieved from getAgentSystemInfo
|
// release the buffer retrieved from getAgentSystemInfo
|
||||||
if bufFromPool, ok := systemInfo.(bytesFromPool); ok {
|
if bufFromPool, ok := systemInfo.(bytesFromPool); ok {
|
||||||
defer allSystemInfoFixedSizePool.Put(bufFromPool.RawMessage)
|
defer bufFromPool.release(bufFromPool.RawMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.NewBuffer(bytesBuf)
|
|
||||||
err := sonic.ConfigDefault.NewEncoder(buf).Encode(map[string]any{
|
err := sonic.ConfigDefault.NewEncoder(buf).Encode(map[string]any{
|
||||||
agentName: systemInfo,
|
agentName: systemInfo,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
agentPkg "github.com/yusing/godoxy/agent/pkg/agent"
|
agentPkg "github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/metrics/period"
|
"github.com/yusing/godoxy/internal/metrics/period"
|
||||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/synk"
|
"github.com/yusing/goutils/synk"
|
||||||
)
|
)
|
||||||
@@ -21,7 +21,7 @@ type SystemInfoRequest struct {
|
|||||||
Period period.Filter `query:"period"`
|
Period period.Filter `query:"period"`
|
||||||
} // @name SystemInfoRequest
|
} // @name SystemInfoRequest
|
||||||
|
|
||||||
type SystemInfoAggregate period.ResponseType[systeminfo.AggregatedJSON] // @name SystemInfoAggregate
|
type SystemInfoAggregate period.ResponseType[systeminfo.Aggregated] // @name SystemInfoAggregate
|
||||||
|
|
||||||
// @x-id "system_info"
|
// @x-id "system_info"
|
||||||
// @BasePath /api/v1
|
// @BasePath /api/v1
|
||||||
@@ -70,12 +70,16 @@ func SystemInfo(c *gin.Context) {
|
|||||||
maps.Copy(c.Writer.Header(), resp.Header)
|
maps.Copy(c.Writer.Header(), resp.Header)
|
||||||
c.Status(resp.StatusCode)
|
c.Status(resp.StatusCode)
|
||||||
|
|
||||||
buf := pool.Get()
|
pool := synk.GetSizedBytesPool()
|
||||||
defer pool.Put(buf)
|
buf := pool.GetSized(16384)
|
||||||
io.CopyBuffer(c.Writer, resp.Body, buf)
|
_, err = io.CopyBuffer(c.Writer, resp.Body, buf)
|
||||||
|
pool.Put(buf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.Error(apitypes.InternalServerError(err, "failed to copy response to client"))
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
agent.ReverseProxy(c.Writer, c.Request, agentPkg.EndpointSystemInfo)
|
agent.ReverseProxy(c.Writer, c.Request, agentPkg.EndpointSystemInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var pool = synk.GetBytesPool()
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/internal/metrics/period"
|
"github.com/yusing/godoxy/internal/metrics/period"
|
||||||
"github.com/yusing/godoxy/internal/metrics/uptime"
|
"github.com/yusing/godoxy/internal/metrics/uptime"
|
||||||
|
|
||||||
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UptimeRequest struct {
|
type UptimeRequest struct {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
"github.com/yusing/godoxy/internal/config"
|
"github.com/yusing/godoxy/internal/config"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "reload"
|
// @x-id "reload"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/internal/route"
|
"github.com/yusing/godoxy/internal/route"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
|
|
||||||
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RoutesByProvider map[string][]route.Route
|
type RoutesByProvider map[string][]route.Route
|
||||||
|
|||||||
361
internal/api/v1/route/playground.go
Normal file
361
internal/api/v1/route/playground.go
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
package routeApi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yusing/godoxy/internal/common"
|
||||||
|
"github.com/yusing/godoxy/internal/route/rules"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RawRule struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
On string `json:"on"`
|
||||||
|
Do string `json:"do"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlaygroundRequest struct {
|
||||||
|
Rules []RawRule `json:"rules" binding:"required"`
|
||||||
|
MockRequest MockRequest `json:"mockRequest"`
|
||||||
|
MockResponse MockResponse `json:"mockResponse"`
|
||||||
|
} // @name PlaygroundRequest
|
||||||
|
|
||||||
|
type MockRequest struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Headers map[string][]string `json:"headers"`
|
||||||
|
Query map[string][]string `json:"query"`
|
||||||
|
Cookies []MockCookie `json:"cookies"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
RemoteIP string `json:"remoteIP"`
|
||||||
|
} // @name MockRequest
|
||||||
|
|
||||||
|
type MockCookie struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
} // @name MockCookie
|
||||||
|
|
||||||
|
type MockResponse struct {
|
||||||
|
StatusCode int `json:"statusCode"`
|
||||||
|
Headers map[string][]string `json:"headers"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
} // @name MockResponse
|
||||||
|
|
||||||
|
type PlaygroundResponse struct {
|
||||||
|
ParsedRules []ParsedRule `json:"parsedRules"`
|
||||||
|
MatchedRules []string `json:"matchedRules"`
|
||||||
|
FinalRequest FinalRequest `json:"finalRequest"`
|
||||||
|
FinalResponse FinalResponse `json:"finalResponse"`
|
||||||
|
ExecutionError gperr.Error `json:"executionError,omitempty"`
|
||||||
|
UpstreamCalled bool `json:"upstreamCalled"`
|
||||||
|
} // @name PlaygroundResponse
|
||||||
|
|
||||||
|
type ParsedRule struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
On string `json:"on"`
|
||||||
|
Do string `json:"do"`
|
||||||
|
ValidationError gperr.Error `json:"validationError,omitempty"`
|
||||||
|
IsResponseRule bool `json:"isResponseRule"`
|
||||||
|
} // @name ParsedRule
|
||||||
|
|
||||||
|
type FinalRequest struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Headers map[string][]string `json:"headers"`
|
||||||
|
Query map[string][]string `json:"query"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
} // @name FinalRequest
|
||||||
|
|
||||||
|
type FinalResponse struct {
|
||||||
|
StatusCode int `json:"statusCode"`
|
||||||
|
Headers map[string][]string `json:"headers"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
} // @name FinalResponse
|
||||||
|
|
||||||
|
// @x-id "playground"
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @Summary Rule Playground
|
||||||
|
// @Description Test rules against mock request/response
|
||||||
|
// @Tags route
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body PlaygroundRequest true "Playground request"
|
||||||
|
// @Success 200 {object} PlaygroundResponse
|
||||||
|
// @Failure 400 {object} apitypes.ErrorResponse
|
||||||
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
|
// @Router /route/playground [post]
|
||||||
|
func Playground(c *gin.Context) {
|
||||||
|
var req PlaygroundRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply defaults
|
||||||
|
if req.MockRequest.Method == "" {
|
||||||
|
req.MockRequest.Method = "GET"
|
||||||
|
}
|
||||||
|
if req.MockRequest.Path == "" {
|
||||||
|
req.MockRequest.Path = "/"
|
||||||
|
}
|
||||||
|
if req.MockRequest.Host == "" {
|
||||||
|
req.MockRequest.Host = "localhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse rules
|
||||||
|
parsedRules, rulesList, parseErr := parseRules(req.Rules)
|
||||||
|
|
||||||
|
// Create mock HTTP request
|
||||||
|
mockReq := createMockRequest(req.MockRequest)
|
||||||
|
|
||||||
|
// Create mock HTTP response writer
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Set initial mock response if provided
|
||||||
|
if req.MockResponse.StatusCode > 0 {
|
||||||
|
recorder.Code = req.MockResponse.StatusCode
|
||||||
|
}
|
||||||
|
if req.MockResponse.Headers != nil {
|
||||||
|
for k, values := range req.MockResponse.Headers {
|
||||||
|
for _, v := range values {
|
||||||
|
recorder.Header().Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.MockResponse.Body != "" {
|
||||||
|
recorder.Body.WriteString(req.MockResponse.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute rules
|
||||||
|
matchedRules := []string{}
|
||||||
|
upstreamCalled := false
|
||||||
|
var executionError gperr.Error
|
||||||
|
|
||||||
|
// Variables to capture modified request state
|
||||||
|
var finalReqMethod, finalReqPath, finalReqHost string
|
||||||
|
var finalReqHeaders http.Header
|
||||||
|
var finalReqQuery url.Values
|
||||||
|
|
||||||
|
if parseErr == nil && len(rulesList) > 0 {
|
||||||
|
// Create upstream handler that records if it was called and captures request state
|
||||||
|
upstreamHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
upstreamCalled = true
|
||||||
|
// Capture the request state when upstream is called
|
||||||
|
finalReqMethod = r.Method
|
||||||
|
finalReqPath = r.URL.Path
|
||||||
|
finalReqHost = r.Host
|
||||||
|
finalReqHeaders = r.Header.Clone()
|
||||||
|
finalReqQuery = r.URL.Query()
|
||||||
|
|
||||||
|
// Debug: also check RequestURI
|
||||||
|
if r.URL.Path != r.URL.RawPath && r.URL.RawPath != "" {
|
||||||
|
finalReqPath = r.URL.RawPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's mock response body, write it during upstream call
|
||||||
|
if req.MockResponse.Body != "" && w.Header().Get("Content-Type") == "" {
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
}
|
||||||
|
if req.MockResponse.StatusCode > 0 {
|
||||||
|
w.WriteHeader(req.MockResponse.StatusCode)
|
||||||
|
}
|
||||||
|
if req.MockResponse.Body != "" {
|
||||||
|
w.Write([]byte(req.MockResponse.Body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build handler with rules
|
||||||
|
handler := rulesList.BuildHandler(upstreamHandler)
|
||||||
|
|
||||||
|
// Execute the handler
|
||||||
|
handlerWithRecover(recorder, mockReq, handler, &executionError)
|
||||||
|
|
||||||
|
// Track which rules matched
|
||||||
|
// Since we can't easily instrument the rules, we'll check each rule manually
|
||||||
|
matchedRules = checkMatchedRules(rulesList, recorder, mockReq)
|
||||||
|
} else if parseErr != nil {
|
||||||
|
executionError = parseErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build final request state
|
||||||
|
// Use captured state if upstream was called, otherwise use current state
|
||||||
|
var finalRequest FinalRequest
|
||||||
|
if upstreamCalled {
|
||||||
|
finalRequest = FinalRequest{
|
||||||
|
Method: finalReqMethod,
|
||||||
|
Path: finalReqPath,
|
||||||
|
Host: finalReqHost,
|
||||||
|
Headers: finalReqHeaders,
|
||||||
|
Query: finalReqQuery,
|
||||||
|
Body: req.MockRequest.Body,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalRequest = FinalRequest{
|
||||||
|
Method: mockReq.Method,
|
||||||
|
Path: mockReq.URL.Path,
|
||||||
|
Host: mockReq.Host,
|
||||||
|
Headers: mockReq.Header,
|
||||||
|
Query: mockReq.URL.Query(),
|
||||||
|
Body: req.MockRequest.Body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build final response state
|
||||||
|
finalResponse := FinalResponse{
|
||||||
|
StatusCode: recorder.Code,
|
||||||
|
Headers: recorder.Header(),
|
||||||
|
Body: recorder.Body.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure status code defaults to 200 if not set
|
||||||
|
if finalResponse.StatusCode == 0 {
|
||||||
|
finalResponse.StatusCode = http.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent null in response
|
||||||
|
if parsedRules == nil {
|
||||||
|
parsedRules = []ParsedRule{}
|
||||||
|
}
|
||||||
|
if matchedRules == nil {
|
||||||
|
matchedRules = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
response := PlaygroundResponse{
|
||||||
|
ParsedRules: parsedRules,
|
||||||
|
MatchedRules: matchedRules,
|
||||||
|
FinalRequest: finalRequest,
|
||||||
|
FinalResponse: finalResponse,
|
||||||
|
ExecutionError: executionError,
|
||||||
|
UpstreamCalled: upstreamCalled,
|
||||||
|
}
|
||||||
|
|
||||||
|
if common.IsTest {
|
||||||
|
c.Set("response", response)
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerWithRecover(w http.ResponseWriter, r *http.Request, h http.HandlerFunc, outErr *gperr.Error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if outErr != nil {
|
||||||
|
*outErr = gperr.Errorf("panic during rule execution: %v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
h(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, gperr.Error) {
|
||||||
|
var parsedRules []ParsedRule
|
||||||
|
var rulesList rules.Rules
|
||||||
|
|
||||||
|
// Parse each rule individually to capture per-rule errors
|
||||||
|
for _, rawRule := range rawRules {
|
||||||
|
var rule rules.Rule
|
||||||
|
|
||||||
|
// Extract fields
|
||||||
|
name := rawRule.Name
|
||||||
|
onStr := rawRule.On
|
||||||
|
doStr := rawRule.Do
|
||||||
|
|
||||||
|
rule.Name = name
|
||||||
|
|
||||||
|
// Parse On
|
||||||
|
var onErr error
|
||||||
|
if onStr != "" {
|
||||||
|
onErr = rule.On.Parse(onStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Do
|
||||||
|
var doErr error
|
||||||
|
if doStr != "" {
|
||||||
|
doErr = rule.Do.Parse(doStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if valid
|
||||||
|
isValid := onErr == nil && doErr == nil
|
||||||
|
validationErr := gperr.Join(gperr.PrependSubject("on", onErr), gperr.PrependSubject("do", doErr))
|
||||||
|
|
||||||
|
parsedRules = append(parsedRules, ParsedRule{
|
||||||
|
Name: name,
|
||||||
|
On: onStr,
|
||||||
|
Do: doStr,
|
||||||
|
ValidationError: validationErr,
|
||||||
|
IsResponseRule: rule.IsResponseRule(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Only add valid rules to execution list
|
||||||
|
if isValid {
|
||||||
|
rulesList = append(rulesList, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedRules, rulesList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMockRequest(mock MockRequest) *http.Request {
|
||||||
|
// Create URL
|
||||||
|
urlStr := mock.Path
|
||||||
|
if len(mock.Query) > 0 {
|
||||||
|
query := url.Values(mock.Query)
|
||||||
|
urlStr = mock.Path + "?" + query.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create request
|
||||||
|
var body io.Reader
|
||||||
|
if mock.Body != "" {
|
||||||
|
body = strings.NewReader(mock.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(mock.Method, urlStr, body)
|
||||||
|
|
||||||
|
// Set host
|
||||||
|
req.Host = mock.Host
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
req.Header = mock.Headers
|
||||||
|
|
||||||
|
// Set cookies
|
||||||
|
if mock.Cookies != nil {
|
||||||
|
for _, cookie := range mock.Cookies {
|
||||||
|
req.AddCookie(&http.Cookie{
|
||||||
|
Name: cookie.Name,
|
||||||
|
Value: cookie.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set remote address
|
||||||
|
if mock.RemoteIP != "" {
|
||||||
|
req.RemoteAddr = mock.RemoteIP + ":0"
|
||||||
|
} else {
|
||||||
|
req.RemoteAddr = "127.0.0.1:0"
|
||||||
|
}
|
||||||
|
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMatchedRules(rulesList rules.Rules, w http.ResponseWriter, r *http.Request) []string {
|
||||||
|
var matched []string
|
||||||
|
|
||||||
|
// Create a ResponseModifier to properly check rules
|
||||||
|
rm := rules.NewResponseModifier(w)
|
||||||
|
|
||||||
|
for _, rule := range rulesList {
|
||||||
|
// Check if rule matches
|
||||||
|
if rule.Check(rm, r) {
|
||||||
|
matched = append(matched, rule.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matched
|
||||||
|
}
|
||||||
229
internal/api/v1/route/playground_test.go
Normal file
229
internal/api/v1/route/playground_test.go
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
package routeApi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlayground(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
request PlaygroundRequest
|
||||||
|
wantStatusCode int
|
||||||
|
checkResponse func(t *testing.T, resp PlaygroundResponse)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple path matching rule",
|
||||||
|
request: PlaygroundRequest{
|
||||||
|
Rules: []RawRule{
|
||||||
|
{
|
||||||
|
Name: "test rule",
|
||||||
|
On: "path /api",
|
||||||
|
Do: "pass",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MockRequest: MockRequest{
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/api",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
checkResponse: func(t *testing.T, resp PlaygroundResponse) {
|
||||||
|
if len(resp.ParsedRules) != 1 {
|
||||||
|
t.Errorf("expected 1 parsed rule, got %d", len(resp.ParsedRules))
|
||||||
|
}
|
||||||
|
if resp.ParsedRules[0].ValidationError != nil {
|
||||||
|
t.Errorf("expected rule to be valid, got error: %v", resp.ParsedRules[0].ValidationError)
|
||||||
|
}
|
||||||
|
if len(resp.MatchedRules) != 1 || resp.MatchedRules[0] != "test rule" {
|
||||||
|
t.Errorf("expected matched rules to be ['test rule'], got %v", resp.MatchedRules)
|
||||||
|
}
|
||||||
|
if !resp.UpstreamCalled {
|
||||||
|
t.Error("expected upstream to be called")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "header matching rule",
|
||||||
|
request: PlaygroundRequest{
|
||||||
|
Rules: []RawRule{
|
||||||
|
{
|
||||||
|
Name: "check user agent",
|
||||||
|
On: "header User-Agent Chrome",
|
||||||
|
Do: "error 403 Forbidden",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MockRequest: MockRequest{
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/",
|
||||||
|
Headers: map[string][]string{
|
||||||
|
"User-Agent": {"Chrome"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
checkResponse: func(t *testing.T, resp PlaygroundResponse) {
|
||||||
|
if len(resp.ParsedRules) != 1 {
|
||||||
|
t.Errorf("expected 1 parsed rule, got %d", len(resp.ParsedRules))
|
||||||
|
}
|
||||||
|
if resp.ParsedRules[0].ValidationError != nil {
|
||||||
|
t.Errorf("expected rule to be valid, got error: %v", resp.ParsedRules[0].ValidationError)
|
||||||
|
}
|
||||||
|
if len(resp.MatchedRules) != 1 {
|
||||||
|
t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules))
|
||||||
|
}
|
||||||
|
if resp.FinalResponse.StatusCode != 403 {
|
||||||
|
t.Errorf("expected status 403, got %d", resp.FinalResponse.StatusCode)
|
||||||
|
}
|
||||||
|
if resp.UpstreamCalled {
|
||||||
|
t.Error("expected upstream not to be called")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid rule syntax",
|
||||||
|
request: PlaygroundRequest{
|
||||||
|
Rules: []RawRule{
|
||||||
|
{
|
||||||
|
Name: "bad rule",
|
||||||
|
On: "invalid_checker something",
|
||||||
|
Do: "pass",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MockRequest: MockRequest{
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
checkResponse: func(t *testing.T, resp PlaygroundResponse) {
|
||||||
|
if len(resp.ParsedRules) != 1 {
|
||||||
|
t.Errorf("expected 1 parsed rule, got %d", len(resp.ParsedRules))
|
||||||
|
}
|
||||||
|
if resp.ParsedRules[0].ValidationError == nil {
|
||||||
|
t.Error("expected validation error to be set")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rewrite path rule",
|
||||||
|
request: PlaygroundRequest{
|
||||||
|
Rules: []RawRule{
|
||||||
|
{
|
||||||
|
Name: "rewrite rule",
|
||||||
|
On: "path glob(/api/*)",
|
||||||
|
Do: "rewrite /api/ /v1/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MockRequest: MockRequest{
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/api/users",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
checkResponse: func(t *testing.T, resp PlaygroundResponse) {
|
||||||
|
if len(resp.ParsedRules) != 1 {
|
||||||
|
t.Errorf("expected 1 parsed rule, got %d", len(resp.ParsedRules))
|
||||||
|
}
|
||||||
|
if resp.ParsedRules[0].ValidationError != nil {
|
||||||
|
t.Errorf("expected rule to be valid, got error: %v", resp.ParsedRules[0].ValidationError)
|
||||||
|
}
|
||||||
|
if !resp.UpstreamCalled {
|
||||||
|
t.Error("expected upstream to be called")
|
||||||
|
}
|
||||||
|
if resp.FinalRequest.Path != "/v1/users" {
|
||||||
|
t.Errorf("expected path to be rewritten to /v1/users, got %s", resp.FinalRequest.Path)
|
||||||
|
}
|
||||||
|
// Note: matched rules tracking has limitations with fresh ResponseModifier
|
||||||
|
// The important thing is that the rewrite actually worked
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "method matching rule",
|
||||||
|
request: PlaygroundRequest{
|
||||||
|
Rules: []RawRule{
|
||||||
|
{
|
||||||
|
Name: "block POST",
|
||||||
|
On: "method POST",
|
||||||
|
Do: `error "405" "Method Not Allowed"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MockRequest: MockRequest{
|
||||||
|
Method: "POST",
|
||||||
|
Path: "/api",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
checkResponse: func(t *testing.T, resp PlaygroundResponse) {
|
||||||
|
if resp.ParsedRules[0].ValidationError != nil {
|
||||||
|
t.Errorf("expected rule to be valid, got error: %v", resp.ParsedRules[0].ValidationError)
|
||||||
|
}
|
||||||
|
if len(resp.MatchedRules) != 1 {
|
||||||
|
t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules))
|
||||||
|
}
|
||||||
|
if resp.FinalResponse.StatusCode != 405 {
|
||||||
|
t.Errorf("expected status 405, got %d", resp.FinalResponse.StatusCode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create request
|
||||||
|
body, _ := json.Marshal(tt.request)
|
||||||
|
req := httptest.NewRequest("POST", "/api/v1/route/playground", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Create response recorder
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Create gin context
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = req
|
||||||
|
|
||||||
|
// Call handler
|
||||||
|
Playground(c)
|
||||||
|
|
||||||
|
// Check status code
|
||||||
|
if w.Code != tt.wantStatusCode {
|
||||||
|
t.Errorf("expected status code %d, got %d", tt.wantStatusCode, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
respAny, ok := c.Get("response")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected response to be set")
|
||||||
|
}
|
||||||
|
resp := respAny.(PlaygroundResponse)
|
||||||
|
|
||||||
|
// Run custom checks
|
||||||
|
if tt.checkResponse != nil {
|
||||||
|
tt.checkResponse(t, resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlaygroundInvalidRequest(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/api/v1/route/playground", bytes.NewReader([]byte(`{}`)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = req
|
||||||
|
|
||||||
|
Playground(c)
|
||||||
|
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
statequery "github.com/yusing/godoxy/internal/config/query"
|
statequery "github.com/yusing/godoxy/internal/config/query"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
|
||||||
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "providers"
|
// @x-id "providers"
|
||||||
@@ -17,7 +19,7 @@ import (
|
|||||||
// @Tags route,websocket
|
// @Tags route,websocket
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} config.RouteProviderListResponse
|
// @Success 200 {array} statequery.RouteProviderListResponse
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /route/providers [get]
|
// @Router /route/providers [get]
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
statequery "github.com/yusing/godoxy/internal/config/query"
|
statequery "github.com/yusing/godoxy/internal/config/query"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListRouteRequest struct {
|
type ListRouteRequest struct {
|
||||||
|
|||||||
@@ -58,3 +58,13 @@ func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AuthOrProceed(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||||
|
err := defaultAuth.CheckToken(r)
|
||||||
|
if err != nil {
|
||||||
|
defaultAuth.LoginHandler(w, r)
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ func TestUserPassLoginCallbackHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
auth.PostAuthCallbackHandler(w, req)
|
auth.PostAuthCallbackHandler(w, req)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
expect.Equal(t, w.Code, http.StatusUnauthorized)
|
expect.Equal(t, w.Code, http.StatusBadRequest)
|
||||||
} else {
|
} else {
|
||||||
setCookie := expect.Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
setCookie := expect.Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
||||||
expect.True(t, setCookie.Name == auth.TokenCookieName())
|
expect.True(t, setCookie.Name == auth.TokenCookieName())
|
||||||
|
|||||||
@@ -59,6 +59,17 @@ func cookieDomain(r *http.Request) string {
|
|||||||
return ".local"
|
return ".local"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the host is an IP address, return an empty string
|
||||||
|
{
|
||||||
|
host, _, err := net.SplitHostPort(reqHost)
|
||||||
|
if err != nil {
|
||||||
|
host = reqHost
|
||||||
|
}
|
||||||
|
if net.ParseIP(host) != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
parts := strutils.SplitRune(reqHost, '.')
|
parts := strutils.SplitRune(reqHost, '.')
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func NewState() config.State {
|
|||||||
entrypoint: entrypoint.NewEntrypoint(),
|
entrypoint: entrypoint.NewEntrypoint(),
|
||||||
task: task.RootTask("config", false),
|
task: task.RootTask("config", false),
|
||||||
tmpLogBuf: tmpLogBuf,
|
tmpLogBuf: tmpLogBuf,
|
||||||
tmpLog: logging.NewLogger(tmpLogBuf),
|
tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ allowlist = [
|
|||||||
"godaddy",
|
"godaddy",
|
||||||
"googledomains",
|
"googledomains",
|
||||||
"hetzner",
|
"hetzner",
|
||||||
# "hostinger", # TODO: uncomment when v4.27.0 is released
|
"hostinger",
|
||||||
"httpreq",
|
"httpreq",
|
||||||
"ionos",
|
"ionos",
|
||||||
"linode",
|
"linode",
|
||||||
@@ -53,6 +53,8 @@ allowlist = [
|
|||||||
"spaceship",
|
"spaceship",
|
||||||
"vercel",
|
"vercel",
|
||||||
"vultr",
|
"vultr",
|
||||||
|
|
||||||
|
"timewebcloud"
|
||||||
]
|
]
|
||||||
|
|
||||||
for name in allowlist:
|
for name in allowlist:
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
module github.com/yusing/godoxy/internal/dnsproviders
|
module github.com/yusing/godoxy/internal/dnsproviders
|
||||||
|
|
||||||
go 1.25.2
|
go 1.25.3
|
||||||
|
|
||||||
replace github.com/yusing/godoxy => ../..
|
replace github.com/yusing/godoxy => ../..
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-acme/lego/v4 v4.26.0
|
github.com/go-acme/lego/v4 v4.27.0
|
||||||
github.com/yusing/godoxy v0.18.6
|
github.com/yusing/godoxy v0.19.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -25,7 +25,7 @@ require (
|
|||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic v1.14.1 // indirect
|
github.com/bytedance/sonic v1.14.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // 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/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
@@ -59,8 +59,8 @@ require (
|
|||||||
github.com/miekg/dns v1.1.68 // indirect
|
github.com/miekg/dns v1.1.68 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.0 // indirect
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1 // indirect
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.0 // indirect
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1 // indirect
|
||||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
@@ -75,7 +75,7 @@ require (
|
|||||||
github.com/vultr/govultr/v3 v3.24.0 // indirect
|
github.com/vultr/govultr/v3 v3.24.0 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
github.com/yusing/gointernals v0.1.16 // indirect
|
github.com/yusing/gointernals v0.1.16 // indirect
|
||||||
github.com/yusing/goutils v0.6.1 // indirect
|
github.com/yusing/goutils v0.7.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||||
@@ -92,8 +92,8 @@ require (
|
|||||||
golang.org/x/sys v0.37.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
golang.org/x/text v0.30.0 // indirect
|
golang.org/x/text v0.30.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
google.golang.org/api v0.252.0 // indirect
|
google.golang.org/api v0.253.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||||
google.golang.org/grpc v1.76.0 // indirect
|
google.golang.org/grpc v1.76.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7
|
|||||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
@@ -55,8 +55,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
|||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/go-acme/lego/v4 v4.26.0 h1:521aEQxNstXvPQcFDDPrJiFfixcCQuvAvm35R4GbyYA=
|
github.com/go-acme/lego/v4 v4.27.0 h1:cIhWd7Uj4BNFLEF3IpwuMkukVVRs5qjlp4KdUGa75yU=
|
||||||
github.com/go-acme/lego/v4 v4.26.0/go.mod h1:BQVAWgcyzW4IT9eIKHY/RxYlVhoyKyOMXOkq7jK1eEQ=
|
github.com/go-acme/lego/v4 v4.27.0/go.mod h1:9FfNZHZmg6hf5CWOp4Lzo4gU8aBEvqZvrwdkBboa+4g=
|
||||||
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=
|
||||||
@@ -137,10 +137,10 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
|
|||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
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/oci-go-sdk/common/v1065 v1065.102.0 h1:W28ZizQSS2aRWkFA3iAP9eiZS4OLFaiv35nXtq2lW/s=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1 h1:45giryNXrlUHzK/Cd4DDBOhaK0EklXrhjTgv00Zo5po=
|
||||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.0/go.mod h1:cVbzGjRhtXgrduaQbR1GR1x+VDU60NcXPMZ3+eQuiiY=
|
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.102.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.0 h1:gAOs1dkE7LFoWflzqrDqAhOprc0kF1a0fyV8C4HUPj4=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1 h1:2EthQw4pEN2rbbSLWlF9itV+Ws2xmAmIcfKYsrwCbVA=
|
||||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.0/go.mod h1:EUBSYwop1K40VpcKy1haIK6kFK/gPT1atEk89OkY0Kg=
|
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.102.1/go.mod h1:xOLJ0zNGmF4M4LqdQclLONwdzjJewNl/7WQiZgrvYR8=
|
||||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||||
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
||||||
@@ -182,8 +182,8 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zU
|
|||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||||
github.com/yusing/goutils v0.6.1 h1:PQmWQEBV+xkI6vnyreQ2uT1PFWTQNkZfHM7Oczuih/s=
|
github.com/yusing/goutils v0.7.0 h1:I5hd8GwZ+3WZqFPK0tWqek1Q5MY6Xg29hKZcwwQi4SY=
|
||||||
github.com/yusing/goutils v0.6.1/go.mod h1:3dgYe/A3+8wT88/iAHwXdL44q5bP+qVo2WAOiPBqOrg=
|
github.com/yusing/goutils v0.7.0/go.mod h1:CtF/KFH4q8jkr7cvBpkaExnudE0lLu8sLe43F73Bn5Q=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||||
@@ -231,14 +231,14 @@ golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs
|
|||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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.252.0 h1:xfKJeAJaMwb8OC9fesr369rjciQ704AjU/psjkKURSI=
|
google.golang.org/api v0.253.0 h1:apU86Eq9Q2eQco3NsUYFpVTfy7DwemojL7LmbAj7g/I=
|
||||||
google.golang.org/api v0.252.0/go.mod h1:dnHOv81x5RAmumZ7BWLShB/u7JZNeyalImxHmtTHxqw=
|
google.golang.org/api v0.253.0/go.mod h1:PX09ad0r/4du83vZVAaGg7OaeyGnaUmT/CYPNvtLCbw=
|
||||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
||||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk=
|
google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/go-acme/lego/v4/providers/dns/godaddy"
|
"github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/googledomains"
|
"github.com/go-acme/lego/v4/providers/dns/googledomains"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/hetzner"
|
"github.com/go-acme/lego/v4/providers/dns/hetzner"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/hostinger"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/httpreq"
|
"github.com/go-acme/lego/v4/providers/dns/httpreq"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/ionos"
|
"github.com/go-acme/lego/v4/providers/dns/ionos"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/linode"
|
"github.com/go-acme/lego/v4/providers/dns/linode"
|
||||||
@@ -27,6 +28,7 @@ import (
|
|||||||
"github.com/go-acme/lego/v4/providers/dns/rfc2136"
|
"github.com/go-acme/lego/v4/providers/dns/rfc2136"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/scaleway"
|
"github.com/go-acme/lego/v4/providers/dns/scaleway"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/spaceship"
|
"github.com/go-acme/lego/v4/providers/dns/spaceship"
|
||||||
|
"github.com/go-acme/lego/v4/providers/dns/timewebcloud"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/vercel"
|
"github.com/go-acme/lego/v4/providers/dns/vercel"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/vultr"
|
"github.com/go-acme/lego/v4/providers/dns/vultr"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
@@ -52,6 +54,7 @@ func InitProviders() {
|
|||||||
autocert.Providers["godaddy"] = autocert.DNSProvider(godaddy.NewDefaultConfig, godaddy.NewDNSProviderConfig)
|
autocert.Providers["godaddy"] = autocert.DNSProvider(godaddy.NewDefaultConfig, godaddy.NewDNSProviderConfig)
|
||||||
autocert.Providers["googledomains"] = autocert.DNSProvider(googledomains.NewDefaultConfig, googledomains.NewDNSProviderConfig)
|
autocert.Providers["googledomains"] = autocert.DNSProvider(googledomains.NewDefaultConfig, googledomains.NewDNSProviderConfig)
|
||||||
autocert.Providers["hetzner"] = autocert.DNSProvider(hetzner.NewDefaultConfig, hetzner.NewDNSProviderConfig)
|
autocert.Providers["hetzner"] = autocert.DNSProvider(hetzner.NewDefaultConfig, hetzner.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["hostinger"] = autocert.DNSProvider(hostinger.NewDefaultConfig, hostinger.NewDNSProviderConfig)
|
||||||
autocert.Providers["httpreq"] = autocert.DNSProvider(httpreq.NewDefaultConfig, httpreq.NewDNSProviderConfig)
|
autocert.Providers["httpreq"] = autocert.DNSProvider(httpreq.NewDefaultConfig, httpreq.NewDNSProviderConfig)
|
||||||
autocert.Providers["ionos"] = autocert.DNSProvider(ionos.NewDefaultConfig, ionos.NewDNSProviderConfig)
|
autocert.Providers["ionos"] = autocert.DNSProvider(ionos.NewDefaultConfig, ionos.NewDNSProviderConfig)
|
||||||
autocert.Providers["linode"] = autocert.DNSProvider(linode.NewDefaultConfig, linode.NewDNSProviderConfig)
|
autocert.Providers["linode"] = autocert.DNSProvider(linode.NewDefaultConfig, linode.NewDNSProviderConfig)
|
||||||
@@ -66,4 +69,5 @@ func InitProviders() {
|
|||||||
autocert.Providers["spaceship"] = autocert.DNSProvider(spaceship.NewDefaultConfig, spaceship.NewDNSProviderConfig)
|
autocert.Providers["spaceship"] = autocert.DNSProvider(spaceship.NewDefaultConfig, spaceship.NewDNSProviderConfig)
|
||||||
autocert.Providers["vercel"] = autocert.DNSProvider(vercel.NewDefaultConfig, vercel.NewDNSProviderConfig)
|
autocert.Providers["vercel"] = autocert.DNSProvider(vercel.NewDefaultConfig, vercel.NewDNSProviderConfig)
|
||||||
autocert.Providers["vultr"] = autocert.DNSProvider(vultr.NewDefaultConfig, vultr.NewDNSProviderConfig)
|
autocert.Providers["vultr"] = autocert.DNSProvider(vultr.NewDefaultConfig, vultr.NewDNSProviderConfig)
|
||||||
|
autocert.Providers["timewebcloud"] = autocert.DNSProvider(timewebcloud.NewDefaultConfig, timewebcloud.NewDNSProviderConfig)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,20 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/connhelper"
|
"github.com/docker/cli/cli/connhelper"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: implement reconnect here.
|
// TODO: implement reconnect here.
|
||||||
@@ -30,6 +34,8 @@ type (
|
|||||||
key string
|
key string
|
||||||
addr string
|
addr string
|
||||||
dial func(ctx context.Context) (net.Conn, error)
|
dial func(ctx context.Context) (net.Conn, error)
|
||||||
|
|
||||||
|
unique bool
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,16 +120,23 @@ func Clients() map[string]*SharedClient {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - Client: the Docker client connection.
|
// - Client: the Docker client connection.
|
||||||
// - error: an error if the connection failed.
|
// - error: an error if the connection failed.
|
||||||
func NewClient(host string) (*SharedClient, error) {
|
func NewClient(host string, unique ...bool) (*SharedClient, error) {
|
||||||
initClientCleanerOnce.Do(initClientCleaner)
|
initClientCleanerOnce.Do(initClientCleaner)
|
||||||
|
|
||||||
clientMapMu.Lock()
|
u := false
|
||||||
defer clientMapMu.Unlock()
|
if len(unique) > 0 {
|
||||||
|
u = unique[0]
|
||||||
|
}
|
||||||
|
|
||||||
if client, ok := clientMap[host]; ok {
|
if !u {
|
||||||
client.closedOn.Store(0)
|
clientMapMu.Lock()
|
||||||
client.refCount.Add(1)
|
defer clientMapMu.Unlock()
|
||||||
return client, nil
|
|
||||||
|
if client, ok := clientMap[host]; ok {
|
||||||
|
client.closedOn.Store(0)
|
||||||
|
client.refCount.Add(1)
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// create client
|
// create client
|
||||||
@@ -188,7 +201,9 @@ func NewClient(host string) (*SharedClient, error) {
|
|||||||
addr: addr,
|
addr: addr,
|
||||||
key: host,
|
key: host,
|
||||||
dial: dial,
|
dial: dial,
|
||||||
|
unique: u,
|
||||||
}
|
}
|
||||||
|
c.unotel()
|
||||||
c.refCount.Store(1)
|
c.refCount.Store(1)
|
||||||
|
|
||||||
// non-agent client
|
// non-agent client
|
||||||
@@ -201,10 +216,28 @@ func NewClient(host string) (*SharedClient, error) {
|
|||||||
|
|
||||||
defer log.Debug().Str("host", host).Msg("docker client initialized")
|
defer log.Debug().Str("host", host).Msg("docker client initialized")
|
||||||
|
|
||||||
clientMap[c.Key()] = c
|
if !u {
|
||||||
|
clientMap[c.Key()] = c
|
||||||
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *SharedClient) GetHTTPClient() **http.Client {
|
||||||
|
return (**http.Client)(unsafe.Pointer(uintptr(unsafe.Pointer(c.Client)) + clientClientOffset))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SharedClient) InterceptHTTPClient(intercept httputils.InterceptFunc) {
|
||||||
|
httpClient := *c.GetHTTPClient()
|
||||||
|
httpClient.Transport = httputils.NewInterceptedTransport(httpClient.Transport, intercept)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SharedClient) CloneUnique() *SharedClient {
|
||||||
|
// there will be no error here
|
||||||
|
// since we are using the same host from a valid client.
|
||||||
|
c, _ = NewClient(c.key, true)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
func (c *SharedClient) Key() string {
|
func (c *SharedClient) Key() string {
|
||||||
return c.key
|
return c.key
|
||||||
}
|
}
|
||||||
@@ -222,8 +255,41 @@ func (c *SharedClient) CheckConnection(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the client is still referenced, this is no-op.
|
// for shared clients, if the client is still referenced, this is no-op.
|
||||||
func (c *SharedClient) Close() {
|
func (c *SharedClient) Close() {
|
||||||
|
if c.unique {
|
||||||
|
c.Client.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
c.closedOn.Store(time.Now().Unix())
|
c.closedOn.Store(time.Now().Unix())
|
||||||
c.refCount.Add(-1)
|
c.refCount.Add(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var clientClientOffset = func() uintptr {
|
||||||
|
field, ok := reflect.TypeFor[client.Client]().FieldByName("client")
|
||||||
|
if !ok {
|
||||||
|
panic("client.Client has no client field")
|
||||||
|
}
|
||||||
|
return field.Offset
|
||||||
|
}()
|
||||||
|
|
||||||
|
var otelRtOffset = func() uintptr {
|
||||||
|
field, ok := reflect.TypeFor[otelhttp.Transport]().FieldByName("rt")
|
||||||
|
if !ok {
|
||||||
|
panic("otelhttp.Transport has no rt field")
|
||||||
|
}
|
||||||
|
return field.Offset
|
||||||
|
}()
|
||||||
|
|
||||||
|
func (c *SharedClient) unotel() {
|
||||||
|
// we don't need and don't want otelhttp.Transport here.
|
||||||
|
httpClient := *c.GetHTTPClient()
|
||||||
|
|
||||||
|
otelTransport, ok := httpClient.Transport.(*otelhttp.Transport)
|
||||||
|
if !ok {
|
||||||
|
log.Debug().Str("host", c.DaemonHost()).Msgf("docker client transport is not an otelhttp.Transport: %T", httpClient.Transport)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
transport := *(*http.RoundTripper)(unsafe.Pointer(uintptr(unsafe.Pointer(otelTransport)) + otelRtOffset))
|
||||||
|
httpClient.Transport = transport
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ func (ep *Entrypoint) SetFindRouteDomains(domains []string) {
|
|||||||
if len(domains) == 0 {
|
if len(domains) == 0 {
|
||||||
ep.findRouteFunc = findRouteAnyDomain
|
ep.findRouteFunc = findRouteAnyDomain
|
||||||
} else {
|
} else {
|
||||||
|
for i, domain := range domains {
|
||||||
|
if !strings.HasPrefix(domain, ".") {
|
||||||
|
domains[i] = "." + domain
|
||||||
|
}
|
||||||
|
}
|
||||||
ep.findRouteFunc = findRouteByDomains(domains)
|
ep.findRouteFunc = findRouteByDomains(domains)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
. "github.com/yusing/godoxy/internal/entrypoint"
|
. "github.com/yusing/godoxy/internal/entrypoint"
|
||||||
"github.com/yusing/godoxy/internal/route"
|
"github.com/yusing/godoxy/internal/route"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
|
routeTypes "github.com/yusing/godoxy/internal/route/types"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
@@ -78,7 +79,7 @@ func BenchmarkEntrypointReal(b *testing.B) {
|
|||||||
|
|
||||||
r := &route.Route{
|
r := &route.Route{
|
||||||
Alias: "test",
|
Alias: "test",
|
||||||
Scheme: "http",
|
Scheme: routeTypes.SchemeHTTP,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: route.Port{Proxy: portInt},
|
Port: route.Port{Proxy: portInt},
|
||||||
HealthCheck: &types.HealthCheckConfig{Disable: true},
|
HealthCheck: &types.HealthCheckConfig{Disable: true},
|
||||||
@@ -119,7 +120,7 @@ func BenchmarkEntrypoint(b *testing.B) {
|
|||||||
|
|
||||||
r := &route.Route{
|
r := &route.Route{
|
||||||
Alias: "test",
|
Alias: "test",
|
||||||
Scheme: "http",
|
Scheme: routeTypes.SchemeHTTP,
|
||||||
Host: "localhost",
|
Host: "localhost",
|
||||||
Port: route.Port{
|
Port: route.Port{
|
||||||
Proxy: 8080,
|
Proxy: 8080,
|
||||||
|
|||||||
Submodule internal/gopsutil updated: 6e3478be65...5f60518fa5
@@ -15,8 +15,8 @@ import (
|
|||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/vincent-petithory/dataurl"
|
"github.com/vincent-petithory/dataurl"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/cache"
|
"github.com/yusing/goutils/cache"
|
||||||
httputils "github.com/yusing/goutils/http"
|
httputils "github.com/yusing/goutils/http"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
@@ -210,7 +210,7 @@ func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string)
|
|||||||
return findIconSlow(ctx, r, loc, append(stack, newReq.URL.Path))
|
return findIconSlow(ctx, r, loc, append(stack, newReq.URL.Path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return FetchResultWithErrorf(c.status, "upstream error: %s", c.data)
|
return FetchResultWithErrorf(c.status, "upstream error: status %d, %s", c.status, c.data)
|
||||||
}
|
}
|
||||||
// return icon data
|
// return icon data
|
||||||
if !httputils.GetContentType(c.header).IsHTML() {
|
if !httputils.GetContentType(c.header).IsHTML() {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package homepage
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -14,6 +13,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/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
"github.com/yusing/goutils/synk"
|
"github.com/yusing/goutils/synk"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
@@ -266,30 +266,26 @@ func updateIcons(m IconMap) error {
|
|||||||
var httpGet = httpGetImpl
|
var httpGet = httpGetImpl
|
||||||
|
|
||||||
func MockHTTPGet(body []byte) {
|
func MockHTTPGet(body []byte) {
|
||||||
httpGet = func(_ string) ([]byte, error) {
|
httpGet = func(_ string) ([]byte, func([]byte), error) {
|
||||||
return body, nil
|
return body, func([]byte) {}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpGetImpl(url string) ([]byte, error) {
|
func httpGetImpl(url string) ([]byte, func([]byte), error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
return httputils.ReadAllBody(resp)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return body, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -308,13 +304,14 @@ format:
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
func UpdateWalkxCodeIcons(m IconMap) error {
|
func UpdateWalkxCodeIcons(m IconMap) error {
|
||||||
body, err := httpGet(walkxcodeIcons)
|
body, release, err := httpGet(walkxcodeIcons)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
data := make(map[string][]string)
|
data := make(map[string][]string)
|
||||||
err = sonic.Unmarshal(body, &data)
|
err = sonic.Unmarshal(body, &data)
|
||||||
|
release(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -379,13 +376,14 @@ func UpdateSelfhstIcons(m IconMap) error {
|
|||||||
Tags string
|
Tags string
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := httpGet(selfhstIcons)
|
body, release, err := httpGet(selfhstIcons)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
data := make([]SelfhStIcon, 0)
|
data := make([]SelfhStIcon, 0)
|
||||||
err = sonic.Unmarshal(body, &data) //nolint:musttag
|
err = sonic.Unmarshal(body, &data) //nolint:musttag
|
||||||
|
release(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package jsonstore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"maps"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -114,7 +113,7 @@ func (s *MapStore[VT]) Initialize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s MapStore[VT]) MarshalJSON() ([]byte, error) {
|
func (s MapStore[VT]) MarshalJSON() ([]byte, error) {
|
||||||
return sonic.Marshal(maps.Collect(s.Range))
|
return sonic.Marshal(xsync.ToPlainMap(s.Map))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MapStore[VT]) UnmarshalJSON(data []byte) error {
|
func (s *MapStore[VT]) UnmarshalJSON(data []byte) error {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const (
|
|||||||
errBurst = 5
|
errBurst = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
var lineBufPool = synk.GetBytesPoolWithUniqueMemory()
|
var bytesPool = synk.GetUnsizedBytesPool()
|
||||||
|
|
||||||
func NewAccessLogger(parent task.Parent, cfg AnyConfig) (*AccessLogger, error) {
|
func NewAccessLogger(parent task.Parent, cfg AnyConfig) (*AccessLogger, error) {
|
||||||
io, err := cfg.IO()
|
io, err := cfg.IO()
|
||||||
@@ -156,13 +156,13 @@ func (l *AccessLogger) Log(req *http.Request, res *http.Response) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
line := lineBufPool.Get()
|
line := bytesPool.Get()
|
||||||
defer lineBufPool.Put(line)
|
|
||||||
line = l.AppendRequestLog(line, req, res)
|
line = l.AppendRequestLog(line, req, res)
|
||||||
if line[len(line)-1] != '\n' {
|
if line[len(line)-1] != '\n' {
|
||||||
line = append(line, '\n')
|
line = append(line, '\n')
|
||||||
}
|
}
|
||||||
l.write(line)
|
l.write(line)
|
||||||
|
bytesPool.Put(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *AccessLogger) LogError(req *http.Request, err error) {
|
func (l *AccessLogger) LogError(req *http.Request, err error) {
|
||||||
@@ -170,13 +170,13 @@ func (l *AccessLogger) LogError(req *http.Request, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *AccessLogger) LogACL(info *maxmind.IPInfo, blocked bool) {
|
func (l *AccessLogger) LogACL(info *maxmind.IPInfo, blocked bool) {
|
||||||
line := lineBufPool.Get()
|
line := bytesPool.Get()
|
||||||
defer lineBufPool.Put(line)
|
|
||||||
line = l.AppendACLLog(line, info, blocked)
|
line = l.AppendACLLog(line, info, blocked)
|
||||||
if line[len(line)-1] != '\n' {
|
if line[len(line)-1] != '\n' {
|
||||||
line = append(line, '\n')
|
line = append(line, '\n')
|
||||||
}
|
}
|
||||||
l.write(line)
|
l.write(line)
|
||||||
|
bytesPool.Put(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *AccessLogger) ShouldRotate() bool {
|
func (l *AccessLogger) ShouldRotate() bool {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func (cfg *ConfigBase) Validate() gperr.Error {
|
|||||||
// If only stdout is enabled, it returns nil, nil.
|
// If only stdout is enabled, it returns nil, nil.
|
||||||
func (cfg *ConfigBase) IO() (WriterWithName, error) {
|
func (cfg *ConfigBase) IO() (WriterWithName, error) {
|
||||||
if cfg.Path != "" {
|
if cfg.Path != "" {
|
||||||
io, err := newFileIO(cfg.Path)
|
io, err := NewFileIO(cfg.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ var (
|
|||||||
openedFilesMu sync.Mutex
|
openedFilesMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func newFileIO(path string) (WriterWithName, error) {
|
// NewFileIO creates a new file writer with cleaned path.
|
||||||
|
//
|
||||||
|
// If the file is already opened, it will be returned.
|
||||||
|
func NewFileIO(path string) (WriterWithName, error) {
|
||||||
openedFilesMu.Lock()
|
openedFilesMu.Lock()
|
||||||
defer openedFilesMu.Unlock()
|
defer openedFilesMu.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func TestConcurrentFileLoggersShareSameAccessLogIO(t *testing.T) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(index int) {
|
go func(index int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
file, err := newFileIO(cfg.Path)
|
file, err := NewFileIO(cfg.Path)
|
||||||
expect.NoError(t, err)
|
expect.NoError(t, err)
|
||||||
accessLogIOs[index] = file
|
accessLogIOs[index] = file
|
||||||
}(i)
|
}(i)
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/yusing/godoxy/internal/utils"
|
"github.com/yusing/godoxy/internal/utils"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
"github.com/yusing/goutils/synk"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type supportRotate interface {
|
type supportRotate interface {
|
||||||
@@ -58,8 +58,6 @@ type lineInfo struct {
|
|||||||
Size int64 // Size of this line
|
Size int64 // Size of this line
|
||||||
}
|
}
|
||||||
|
|
||||||
var rotateBytePool = synk.GetBytesPoolWithUniqueMemory()
|
|
||||||
|
|
||||||
// rotateLogFile rotates the log file based on the retention policy.
|
// rotateLogFile rotates the log file based on the retention policy.
|
||||||
// It writes to the result and returns an error if any.
|
// It writes to the result and returns an error if any.
|
||||||
//
|
//
|
||||||
@@ -166,15 +164,17 @@ func rotateLogFileByPolicy(file supportRotate, config *Retention, result *Rotate
|
|||||||
|
|
||||||
// Read each line and write it to the beginning of the file
|
// Read each line and write it to the beginning of the file
|
||||||
writePos := int64(0)
|
writePos := int64(0)
|
||||||
buf := rotateBytePool.Get()
|
buf := bytesPool.Get()
|
||||||
defer rotateBytePool.Put(buf)
|
defer func() {
|
||||||
|
bytesPool.Put(buf)
|
||||||
|
}()
|
||||||
|
|
||||||
// in reverse order to keep the order of the lines (from old to new)
|
// in reverse order to keep the order of the lines (from old to new)
|
||||||
for i := len(linesToKeep) - 1; i >= 0; i-- {
|
for i := len(linesToKeep) - 1; i >= 0; i-- {
|
||||||
line := linesToKeep[i]
|
line := linesToKeep[i]
|
||||||
n := line.Size
|
n := line.Size
|
||||||
if cap(buf) < int(n) {
|
if cap(buf) < int(n) {
|
||||||
buf = make([]byte, n)
|
buf = slices.Grow(buf, int(n)-cap(buf))
|
||||||
}
|
}
|
||||||
buf = buf[:n]
|
buf = buf[:n]
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,19 @@ import (
|
|||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
|
||||||
|
|
||||||
zerologlog "github.com/rs/zerolog/log"
|
zerologlog "github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func InitLogger(out ...io.Writer) {
|
||||||
|
logger = NewLogger(out...)
|
||||||
|
log.SetOutput(logger)
|
||||||
|
log.SetPrefix("")
|
||||||
|
log.SetFlags(0)
|
||||||
|
zerolog.TimeFieldFormat = timeFmt
|
||||||
|
zerologlog.Logger = logger
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logger zerolog.Logger
|
logger zerolog.Logger
|
||||||
timeFmt string
|
timeFmt string
|
||||||
@@ -38,34 +46,62 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fmtMessage(msg string) string {
|
func fmtMessage(msg string) string {
|
||||||
lines := strutils.SplitRune(msg, '\n')
|
nLines := strings.Count(msg, "\n")
|
||||||
if len(lines) == 1 {
|
if nLines == 0 {
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
for i := 1; i < len(lines); i++ {
|
|
||||||
lines[i] = prefix + lines[i]
|
var sb strings.Builder
|
||||||
|
sb.Grow(len(msg) + nLines*len(prefix))
|
||||||
|
|
||||||
|
// write first line unindented
|
||||||
|
idx := strings.IndexByte(msg, '\n')
|
||||||
|
sb.WriteString(msg[:idx])
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
msg = msg[idx+1:]
|
||||||
|
|
||||||
|
// write remaining lines indented
|
||||||
|
for line := range strings.Lines(msg) {
|
||||||
|
sb.WriteString(prefix)
|
||||||
|
sb.WriteString(line)
|
||||||
}
|
}
|
||||||
return strutils.JoinRune(lines, '\n')
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func multiLevelWriter(out ...io.Writer) io.Writer {
|
||||||
|
if len(out) == 0 {
|
||||||
|
return os.Stdout
|
||||||
|
}
|
||||||
|
if len(out) == 1 {
|
||||||
|
return out[0]
|
||||||
|
}
|
||||||
|
return io.MultiWriter(out...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogger(out ...io.Writer) zerolog.Logger {
|
func NewLogger(out ...io.Writer) zerolog.Logger {
|
||||||
writer := zerolog.ConsoleWriter{
|
writer := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||||
Out: zerolog.MultiLevelWriter(out...),
|
w.Out = multiLevelWriter(out...)
|
||||||
TimeFormat: timeFmt,
|
w.TimeFormat = timeFmt
|
||||||
FormatMessage: func(msgI interface{}) string { // pad spaces for each line
|
w.FormatMessage = func(msgI any) string { // pad spaces for each line
|
||||||
|
if msgI == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return fmtMessage(msgI.(string))
|
return fmtMessage(msgI.(string))
|
||||||
},
|
}
|
||||||
}
|
})
|
||||||
return zerolog.New(
|
return zerolog.New(writer).Level(level).With().Timestamp().Logger()
|
||||||
writer,
|
|
||||||
).Level(level).With().Timestamp().Logger()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitLogger(out ...io.Writer) {
|
func NewLoggerWithFixedLevel(level zerolog.Level, out ...io.Writer) zerolog.Logger {
|
||||||
logger = NewLogger(out...)
|
writer := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||||
log.SetOutput(logger)
|
w.Out = multiLevelWriter(out...)
|
||||||
log.SetPrefix("")
|
w.TimeFormat = timeFmt
|
||||||
log.SetFlags(0)
|
w.FormatMessage = func(msgI any) string { // pad spaces for each line
|
||||||
zerolog.TimeFieldFormat = timeFmt
|
if msgI == nil {
|
||||||
zerologlog.Logger = logger
|
return ""
|
||||||
|
}
|
||||||
|
return fmtMessage(msgI.(string))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return zerolog.New(writer).Level(level).With().Str("level", level.String()).Timestamp().Logger()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/puzpuzpuz/xsync/v4"
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package period
|
package period
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
@@ -68,19 +69,22 @@ func (e *Entries[T]) Get() []T {
|
|||||||
return res[:]
|
return res[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type entriesJSON[T any] struct {
|
||||||
|
Entries []T `json:"entries"`
|
||||||
|
Interval time.Duration `json:"interval"`
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Entries[T]) MarshalJSON() ([]byte, error) {
|
func (e *Entries[T]) MarshalJSON() ([]byte, error) {
|
||||||
return sonic.Marshal(map[string]any{
|
return sonic.Marshal(entriesJSON[T]{
|
||||||
"entries": e.Get(),
|
Entries: e.Get(),
|
||||||
"interval": e.interval,
|
Interval: e.interval,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Entries[T]) UnmarshalJSON(data []byte) error {
|
func (e *Entries[T]) UnmarshalJSON(data []byte) error {
|
||||||
var v struct {
|
var v entriesJSON[T]
|
||||||
Entries []T `json:"entries"`
|
v.Entries = make([]T, 0, maxEntries)
|
||||||
Interval time.Duration `json:"interval"`
|
if err := json.Unmarshal(data, &v); err != nil {
|
||||||
}
|
|
||||||
if err := sonic.Unmarshal(data, &v); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(v.Entries) == 0 {
|
if len(v.Entries) == 0 {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
|
||||||
metricsutils "github.com/yusing/godoxy/internal/metrics/utils"
|
metricsutils "github.com/yusing/godoxy/internal/metrics/utils"
|
||||||
|
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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package period
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -72,11 +73,16 @@ func (p *Poller[T, AggregateT]) savePath() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller[T, AggregateT]) load() error {
|
func (p *Poller[T, AggregateT]) load() error {
|
||||||
entries, err := os.ReadFile(p.savePath())
|
content, err := os.ReadFile(p.savePath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := sonic.Unmarshal(entries, &p.period); err != nil {
|
|
||||||
|
if len(content) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(content, p.period); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Validate and fix intervals after loading to ensure data integrity.
|
// Validate and fix intervals after loading to ensure data integrity.
|
||||||
@@ -86,11 +92,17 @@ func (p *Poller[T, AggregateT]) load() error {
|
|||||||
|
|
||||||
func (p *Poller[T, AggregateT]) save() error {
|
func (p *Poller[T, AggregateT]) save() error {
|
||||||
initDataDirOnce.Do(initDataDir)
|
initDataDirOnce.Do(initDataDir)
|
||||||
entries, err := sonic.Marshal(p.period)
|
f, err := os.OpenFile(p.savePath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(p.savePath(), entries, 0o644)
|
defer f.Close()
|
||||||
|
|
||||||
|
err = sonic.ConfigDefault.NewEncoder(f).Encode(p.period)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller[T, AggregateT]) WithResultFilter(filter FilterFunc[T]) *Poller[T, AggregateT] {
|
func (p *Poller[T, AggregateT]) WithResultFilter(filter FilterFunc[T]) *Poller[T, AggregateT] {
|
||||||
@@ -114,15 +126,15 @@ func (p *Poller[T, AggregateT]) appendErr(err error) {
|
|||||||
p.errs = append(p.errs, pollErr{err: err, count: 1})
|
p.errs = append(p.errs, pollErr{err: err, count: 1})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller[T, AggregateT]) gatherErrs() (string, bool) {
|
func (p *Poller[T, AggregateT]) gatherErrs() (error, bool) {
|
||||||
if len(p.errs) == 0 {
|
if len(p.errs) == 0 {
|
||||||
return "", false
|
return nil, false
|
||||||
}
|
}
|
||||||
errs := gperr.NewBuilder(fmt.Sprintf("poller %s has encountered %d errors in the last %s:", p.name, len(p.errs), gatherErrsInterval))
|
var errs gperr.Builder
|
||||||
for _, e := range p.errs {
|
for _, e := range p.errs {
|
||||||
errs.Addf("%w: %d times", e.err, e.count)
|
errs.Addf("%w: %d times", e.err, e.count)
|
||||||
}
|
}
|
||||||
return errs.String(), true
|
return errs.Error(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller[T, AggregateT]) clearErrs() {
|
func (p *Poller[T, AggregateT]) clearErrs() {
|
||||||
@@ -164,6 +176,7 @@ func (p *Poller[T, AggregateT]) Start() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
l.Err(err).Msg("failed to save metrics data")
|
l.Err(err).Msg("failed to save metrics data")
|
||||||
}
|
}
|
||||||
|
l.Debug().Int("entries", p.period.Total()).Msg("poller finished and saved")
|
||||||
t.Finish(err)
|
t.Finish(err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -183,7 +196,7 @@ func (p *Poller[T, AggregateT]) Start() {
|
|||||||
if tickCount%gatherErrsTicks == 0 {
|
if tickCount%gatherErrsTicks == 0 {
|
||||||
errs, ok := p.gatherErrs()
|
errs, ok := p.gatherErrs()
|
||||||
if ok {
|
if ok {
|
||||||
log.Error().Msg(errs)
|
gperr.LogError(fmt.Sprintf("poller %s has encountered %d errors in the last %s:", p.name, len(p.errs), gatherErrsInterval), errs)
|
||||||
}
|
}
|
||||||
p.clearErrs()
|
p.clearErrs()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,7 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
Sensors []sensors.TemperatureStat // @name Sensors
|
Sensors []sensors.TemperatureStat // @name Sensors
|
||||||
Aggregated struct {
|
Aggregated []map[string]any
|
||||||
Entries []map[string]any
|
|
||||||
Mode SystemInfoAggregateMode
|
|
||||||
}
|
|
||||||
AggregatedJSON []map[string]any
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SystemInfo struct {
|
type SystemInfo struct {
|
||||||
@@ -179,12 +175,12 @@ func (s *SystemInfo) collectDisksInfo(ctx context.Context, lastResult *SystemInf
|
|||||||
s.Disks = make(map[string]disk.UsageStat, len(partitions))
|
s.Disks = make(map[string]disk.UsageStat, len(partitions))
|
||||||
errs := gperr.NewBuilder("failed to get disks info")
|
errs := gperr.NewBuilder("failed to get disks info")
|
||||||
for _, partition := range partitions {
|
for _, partition := range partitions {
|
||||||
diskInfo, err := disk.UsageWithContext(ctx, partition.Mountpoint)
|
diskInfo, err := disk.UsageWithContext(ctx, partition.Mountpoint.Value())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Add(err)
|
errs.Add(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.Disks[partition.Device] = diskInfo
|
s.Disks[partition.Device.Value()] = diskInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
if errs.HasError() {
|
if errs.HasError() {
|
||||||
@@ -222,15 +218,12 @@ func (s *SystemInfo) collectSensorsInfo(ctx context.Context) error {
|
|||||||
// recharts friendly.
|
// recharts friendly.
|
||||||
func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggregated) {
|
func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggregated) {
|
||||||
n := len(entries)
|
n := len(entries)
|
||||||
aggregated := Aggregated{
|
aggregated := make([]map[string]any, 0, n)
|
||||||
Entries: make([]map[string]any, 0, n),
|
switch SystemInfoAggregateMode(query.Get("aggregate")) {
|
||||||
Mode: SystemInfoAggregateMode(query.Get("aggregate")),
|
|
||||||
}
|
|
||||||
switch aggregated.Mode {
|
|
||||||
case SystemInfoAggregateModeCPUAverage:
|
case SystemInfoAggregateModeCPUAverage:
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.CPUAverage != nil {
|
if entry.CPUAverage != nil {
|
||||||
aggregated.Entries = append(aggregated.Entries, map[string]any{
|
aggregated = append(aggregated, map[string]any{
|
||||||
"timestamp": entry.Timestamp,
|
"timestamp": entry.Timestamp,
|
||||||
"cpu_average": *entry.CPUAverage,
|
"cpu_average": *entry.CPUAverage,
|
||||||
})
|
})
|
||||||
@@ -239,7 +232,7 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
|||||||
case SystemInfoAggregateModeMemoryUsage:
|
case SystemInfoAggregateModeMemoryUsage:
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.Memory.Used > 0 {
|
if entry.Memory.Used > 0 {
|
||||||
aggregated.Entries = append(aggregated.Entries, map[string]any{
|
aggregated = append(aggregated, map[string]any{
|
||||||
"timestamp": entry.Timestamp,
|
"timestamp": entry.Timestamp,
|
||||||
"memory_usage": entry.Memory.Used,
|
"memory_usage": entry.Memory.Used,
|
||||||
})
|
})
|
||||||
@@ -247,10 +240,10 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
|||||||
}
|
}
|
||||||
case SystemInfoAggregateModeMemoryUsagePercent:
|
case SystemInfoAggregateModeMemoryUsagePercent:
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.Memory.UsedPercent > 0 {
|
if percent := entry.Memory.UsedPercent(); percent > 0 {
|
||||||
aggregated.Entries = append(aggregated.Entries, map[string]any{
|
aggregated = append(aggregated, map[string]any{
|
||||||
"timestamp": entry.Timestamp,
|
"timestamp": entry.Timestamp,
|
||||||
"memory_usage_percent": entry.Memory.UsedPercent,
|
"memory_usage_percent": percent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,7 +257,7 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
|||||||
m[name] = usage.ReadSpeed
|
m[name] = usage.ReadSpeed
|
||||||
}
|
}
|
||||||
m["timestamp"] = entry.Timestamp
|
m["timestamp"] = entry.Timestamp
|
||||||
aggregated.Entries = append(aggregated.Entries, m)
|
aggregated = append(aggregated, m)
|
||||||
}
|
}
|
||||||
case SystemInfoAggregateModeDisksWriteSpeed:
|
case SystemInfoAggregateModeDisksWriteSpeed:
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
@@ -276,7 +269,7 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
|||||||
m[name] = usage.WriteSpeed
|
m[name] = usage.WriteSpeed
|
||||||
}
|
}
|
||||||
m["timestamp"] = entry.Timestamp
|
m["timestamp"] = entry.Timestamp
|
||||||
aggregated.Entries = append(aggregated.Entries, m)
|
aggregated = append(aggregated, m)
|
||||||
}
|
}
|
||||||
case SystemInfoAggregateModeDisksIOPS:
|
case SystemInfoAggregateModeDisksIOPS:
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
@@ -288,7 +281,7 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
|||||||
m[name] = usage.Iops
|
m[name] = usage.Iops
|
||||||
}
|
}
|
||||||
m["timestamp"] = entry.Timestamp
|
m["timestamp"] = entry.Timestamp
|
||||||
aggregated.Entries = append(aggregated.Entries, m)
|
aggregated = append(aggregated, m)
|
||||||
}
|
}
|
||||||
case SystemInfoAggregateModeDiskUsage:
|
case SystemInfoAggregateModeDiskUsage:
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
@@ -300,14 +293,14 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
|||||||
m[name] = disk.Used
|
m[name] = disk.Used
|
||||||
}
|
}
|
||||||
m["timestamp"] = entry.Timestamp
|
m["timestamp"] = entry.Timestamp
|
||||||
aggregated.Entries = append(aggregated.Entries, m)
|
aggregated = append(aggregated, m)
|
||||||
}
|
}
|
||||||
case SystemInfoAggregateModeNetworkSpeed:
|
case SystemInfoAggregateModeNetworkSpeed:
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.Network.BytesSent == 0 && entry.Network.BytesRecv == 0 {
|
if entry.Network.BytesSent == 0 && entry.Network.BytesRecv == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
aggregated.Entries = append(aggregated.Entries, map[string]any{
|
aggregated = append(aggregated, map[string]any{
|
||||||
"timestamp": entry.Timestamp,
|
"timestamp": entry.Timestamp,
|
||||||
"upload": entry.Network.UploadSpeed,
|
"upload": entry.Network.UploadSpeed,
|
||||||
"download": entry.Network.DownloadSpeed,
|
"download": entry.Network.DownloadSpeed,
|
||||||
@@ -316,13 +309,12 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
|||||||
case SystemInfoAggregateModeNetworkTransfer:
|
case SystemInfoAggregateModeNetworkTransfer:
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.Network.BytesRecv > 0 || entry.Network.BytesSent > 0 {
|
if entry.Network.BytesRecv > 0 || entry.Network.BytesSent > 0 {
|
||||||
continue
|
aggregated = append(aggregated, map[string]any{
|
||||||
|
"timestamp": entry.Timestamp,
|
||||||
|
"upload": entry.Network.BytesSent,
|
||||||
|
"download": entry.Network.BytesRecv,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
aggregated.Entries = append(aggregated.Entries, map[string]any{
|
|
||||||
"timestamp": entry.Timestamp,
|
|
||||||
"upload": entry.Network.BytesSent,
|
|
||||||
"download": entry.Network.BytesRecv,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
case SystemInfoAggregateModeSensorTemperature:
|
case SystemInfoAggregateModeSensorTemperature:
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
@@ -331,15 +323,15 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
|||||||
}
|
}
|
||||||
m := make(map[string]any, len(entry.Sensors)+1)
|
m := make(map[string]any, len(entry.Sensors)+1)
|
||||||
for _, sensor := range entry.Sensors {
|
for _, sensor := range entry.Sensors {
|
||||||
m[sensor.SensorKey] = sensor.Temperature
|
m[sensor.SensorKey.Value()] = sensor.Temperature
|
||||||
}
|
}
|
||||||
m["timestamp"] = entry.Timestamp
|
m["timestamp"] = entry.Timestamp
|
||||||
aggregated.Entries = append(aggregated.Entries, m)
|
aggregated = append(aggregated, m)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return -1, Aggregated{}
|
return -1, nil
|
||||||
}
|
}
|
||||||
return len(aggregated.Entries), aggregated
|
return len(aggregated), aggregated
|
||||||
}
|
}
|
||||||
|
|
||||||
func diff(x, y uint64) uint64 {
|
func diff(x, y uint64) uint64 {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
StatusByAlias struct {
|
StatusByAlias struct {
|
||||||
Map map[string]routes.HealthInfo `json:"statuses"`
|
Map map[string]routes.HealthInfoWithoutDetail `json:"statuses"`
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
} // @name RouteStatusesByAlias
|
} // @name RouteStatusesByAlias
|
||||||
Status struct {
|
Status struct {
|
||||||
Status types.HealthStatus `json:"status" swaggertype:"string" enums:"healthy,unhealthy,unknown,napping,starting"`
|
Status types.HealthStatus `json:"status" swaggertype:"string" enums:"healthy,unhealthy,unknown,napping,starting"`
|
||||||
@@ -44,7 +44,7 @@ var Poller = period.NewPoller("uptime", getStatuses, aggregateStatuses)
|
|||||||
|
|
||||||
func getStatuses(ctx context.Context, _ StatusByAlias) (StatusByAlias, error) {
|
func getStatuses(ctx context.Context, _ StatusByAlias) (StatusByAlias, error) {
|
||||||
return StatusByAlias{
|
return StatusByAlias{
|
||||||
Map: routes.GetHealthInfo(),
|
Map: routes.GetHealthInfoWithoutDetail(),
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,9 @@ import (
|
|||||||
|
|
||||||
type Bypass []rules.RuleOn
|
type Bypass []rules.RuleOn
|
||||||
|
|
||||||
func (b Bypass) ShouldBypass(r *http.Request) bool {
|
func (b Bypass) ShouldBypass(w http.ResponseWriter, r *http.Request) bool {
|
||||||
cached := rules.NewCache()
|
|
||||||
defer cached.Release()
|
|
||||||
for _, rule := range b {
|
for _, rule := range b {
|
||||||
if rule.Check(cached, r) {
|
if rule.Check(w, r) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,14 +24,14 @@ type checkBypass struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *checkBypass) before(w http.ResponseWriter, r *http.Request) (proceedNext bool) {
|
func (c *checkBypass) before(w http.ResponseWriter, r *http.Request) (proceedNext bool) {
|
||||||
if c.modReq == nil || c.bypass.ShouldBypass(r) {
|
if c.modReq == nil || c.bypass.ShouldBypass(w, r) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return c.modReq.before(w, r)
|
return c.modReq.before(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *checkBypass) modifyResponse(resp *http.Response) error {
|
func (c *checkBypass) modifyResponse(w http.ResponseWriter, resp *http.Response) error {
|
||||||
if c.modRes == nil || c.bypass.ShouldBypass(resp.Request) {
|
if c.modRes == nil || c.bypass.ShouldBypass(w, resp.Request) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return c.modRes.modifyResponse(resp)
|
return c.modRes.modifyResponse(resp)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func TestBypassCIDR(t *testing.T) {
|
|||||||
|
|
||||||
func TestBypassPath(t *testing.T) {
|
func TestBypassPath(t *testing.T) {
|
||||||
mr, err := ModifyRequest.New(map[string]any{
|
mr, err := ModifyRequest.New(map[string]any{
|
||||||
"bypass": []string{"path /test/*", "path /api"},
|
"bypass": []string{"path glob(/test/*)", "path /api"},
|
||||||
"set_headers": map[string]string{
|
"set_headers": map[string]string{
|
||||||
"Test-Header": "test-value",
|
"Test-Header": "test-value",
|
||||||
},
|
},
|
||||||
@@ -106,7 +106,7 @@ func TestReverseProxyBypass(t *testing.T) {
|
|||||||
rp := reverseproxy.NewReverseProxy("test", url, fakeRoundTripper{})
|
rp := reverseproxy.NewReverseProxy("test", url, fakeRoundTripper{})
|
||||||
err = PatchReverseProxy(rp, map[string]OptionsRaw{
|
err = PatchReverseProxy(rp, map[string]OptionsRaw{
|
||||||
"response": {
|
"response": {
|
||||||
"bypass": "path /test/* | path /api",
|
"bypass": "path glob(/test/*) | path /api",
|
||||||
"set_headers": map[string]string{
|
"set_headers": map[string]string{
|
||||||
"Test-Header": "test-value",
|
"Test-Header": "test-value",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ type modifyHTML struct {
|
|||||||
Target string // css selector
|
Target string // css selector
|
||||||
HTML string // html to inject
|
HTML string // html to inject
|
||||||
Replace bool // replace the target element with the new html instead of appending it
|
Replace bool // replace the target element with the new html instead of appending it
|
||||||
bytesPool *synk.BytesPool
|
bytesPool synk.UnsizedBytesPool
|
||||||
}
|
}
|
||||||
|
|
||||||
var ModifyHTML = NewMiddleware[modifyHTML]()
|
var ModifyHTML = NewMiddleware[modifyHTML]()
|
||||||
|
|
||||||
func (m *modifyHTML) setup() {
|
func (m *modifyHTML) setup() {
|
||||||
m.bytesPool = synk.GetBytesPool()
|
m.bytesPool = synk.GetUnsizedBytesPool()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modifyHTML) before(_ http.ResponseWriter, req *http.Request) bool {
|
func (m *modifyHTML) before(_ http.ResponseWriter, req *http.Request) bool {
|
||||||
@@ -52,13 +52,13 @@ func (m *modifyHTML) modifyResponse(resp *http.Response) error {
|
|||||||
|
|
||||||
// NOTE: do not put it in the defer, it will be used as resp.Body
|
// NOTE: do not put it in the defer, it will be used as resp.Body
|
||||||
content, release, err := httputils.ReadAllBody(resp)
|
content, release, err := httputils.ReadAllBody(resp)
|
||||||
|
resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Str("url", fullURL(resp.Request)).Msg("failed to read response body")
|
log.Err(err).Str("url", fullURL(resp.Request)).Msg("failed to read response body")
|
||||||
resp.Body.Close()
|
release(content)
|
||||||
resp.Body = eofReader{}
|
resp.Body = eofReader{}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(content))
|
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -83,7 +83,11 @@ func (m *modifyHTML) modifyResponse(resp *http.Response) error {
|
|||||||
ele.First().AppendHtml(m.HTML)
|
ele.First().AppendHtml(m.HTML)
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.NewBuffer(content[:0])
|
// should not use content (from sized pool) directly for bytes.Buffer
|
||||||
|
buf := m.bytesPool.GetBuffer()
|
||||||
|
buf.Write(content)
|
||||||
|
release(content)
|
||||||
|
|
||||||
err = buildHTML(doc, buf)
|
err = buildHTML(doc, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Str("url", fullURL(resp.Request)).Msg("failed to build html")
|
log.Err(err).Str("url", fullURL(resp.Request)).Msg("failed to build html")
|
||||||
@@ -95,8 +99,7 @@ func (m *modifyHTML) modifyResponse(resp *http.Response) error {
|
|||||||
resp.Header.Set("Content-Length", strconv.Itoa(buf.Len()))
|
resp.Header.Set("Content-Length", strconv.Itoa(buf.Len()))
|
||||||
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
|
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
|
||||||
resp.Body = readerWithRelease(buf.Bytes(), func(_ []byte) {
|
resp.Body = readerWithRelease(buf.Bytes(), func(_ []byte) {
|
||||||
// release content, not buf.Bytes()
|
m.bytesPool.PutBuffer(buf)
|
||||||
release(content)
|
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package nettypes
|
|||||||
import (
|
import (
|
||||||
urlPkg "net/url"
|
urlPkg "net/url"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
"github.com/yusing/godoxy/internal/utils"
|
"github.com/yusing/godoxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ func (u *URL) MarshalJSON() (text []byte, err error) {
|
|||||||
if u == nil {
|
if u == nil {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
}
|
}
|
||||||
return []byte("\"" + u.URL.String() + "\""), nil
|
return sonic.Marshal(u.URL.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *URL) Equals(other *URL) bool {
|
func (u *URL) Equals(other *URL) bool {
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
FieldsBody []LogField
|
FieldsBody []LogField
|
||||||
ListBody []string
|
ListBody []string
|
||||||
MessageBody string
|
MessageBody string
|
||||||
errorBody struct {
|
MessageBodyBytes []byte
|
||||||
|
errorBody struct {
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -98,7 +99,15 @@ func (m MessageBody) Format(format LogFormat) ([]byte, error) {
|
|||||||
case LogFormatRawJSON:
|
case LogFormatRawJSON:
|
||||||
return sonic.Marshal(m)
|
return sonic.Marshal(m)
|
||||||
}
|
}
|
||||||
return m.Format(LogFormatMarkdown)
|
return []byte(m), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MessageBodyBytes) Format(format LogFormat) ([]byte, error) {
|
||||||
|
switch format {
|
||||||
|
case LogFormatRawJSON:
|
||||||
|
return sonic.Marshal(string(m))
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e errorBody) Format(format LogFormat) ([]byte, error) {
|
func (e errorBody) Format(format LogFormat) ([]byte, error) {
|
||||||
|
|||||||
@@ -7,18 +7,18 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
F "github.com/yusing/godoxy/internal/utils/functional"
|
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Dispatcher struct {
|
Dispatcher struct {
|
||||||
task *task.Task
|
task *task.Task
|
||||||
providers F.Set[Provider]
|
providers *xsync.Map[Provider, struct{}]
|
||||||
logCh chan *LogMessage
|
logCh chan *LogMessage
|
||||||
retryMsg F.Set[*RetryMessage]
|
retryMsg *xsync.Map[*RetryMessage, struct{}]
|
||||||
retryTicker *time.Ticker
|
retryTicker *time.Ticker
|
||||||
}
|
}
|
||||||
LogMessage struct {
|
LogMessage struct {
|
||||||
@@ -44,9 +44,9 @@ const (
|
|||||||
func StartNotifDispatcher(parent task.Parent) *Dispatcher {
|
func StartNotifDispatcher(parent task.Parent) *Dispatcher {
|
||||||
dispatcher = &Dispatcher{
|
dispatcher = &Dispatcher{
|
||||||
task: parent.Subtask("notification", true),
|
task: parent.Subtask("notification", true),
|
||||||
providers: F.NewSet[Provider](),
|
providers: xsync.NewMap[Provider, struct{}](),
|
||||||
logCh: make(chan *LogMessage, 100),
|
logCh: make(chan *LogMessage, 100),
|
||||||
retryMsg: F.NewSet[*RetryMessage](),
|
retryMsg: xsync.NewMap[*RetryMessage, struct{}](),
|
||||||
retryTicker: time.NewTicker(retryInterval),
|
retryTicker: time.NewTicker(retryInterval),
|
||||||
}
|
}
|
||||||
go dispatcher.start()
|
go dispatcher.start()
|
||||||
@@ -66,7 +66,7 @@ func Notify(msg *LogMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (disp *Dispatcher) RegisterProvider(cfg *NotificationConfig) {
|
func (disp *Dispatcher) RegisterProvider(cfg *NotificationConfig) {
|
||||||
disp.providers.Add(cfg.Provider)
|
disp.providers.Store(cfg.Provider, struct{}{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (disp *Dispatcher) start() {
|
func (disp *Dispatcher) start() {
|
||||||
@@ -115,7 +115,7 @@ func (disp *Dispatcher) dispatch(msg *LogMessage) {
|
|||||||
Provider: p,
|
Provider: p,
|
||||||
NextRetry: time.Now().Add(calculateBackoffDelay(0)),
|
NextRetry: time.Now().Add(calculateBackoffDelay(0)),
|
||||||
}
|
}
|
||||||
disp.retryMsg.Add(msg)
|
disp.retryMsg.Store(msg, struct{}{})
|
||||||
l.Debug().Err(err).EmbedObject(msg).Msg("notification failed, scheduling retry")
|
l.Debug().Err(err).EmbedObject(msg).Msg("notification failed, scheduling retry")
|
||||||
} else {
|
} else {
|
||||||
l.Debug().Str("provider", p.GetName()).Msg("notification sent successfully")
|
l.Debug().Str("provider", p.GetName()).Msg("notification sent successfully")
|
||||||
@@ -136,7 +136,7 @@ func (disp *Dispatcher) processRetries() {
|
|||||||
for msg := range disp.retryMsg.Range {
|
for msg := range disp.retryMsg.Range {
|
||||||
if now.After(msg.NextRetry) {
|
if now.After(msg.NextRetry) {
|
||||||
readyMessages = append(readyMessages, msg)
|
readyMessages = append(readyMessages, msg)
|
||||||
disp.retryMsg.Remove(msg)
|
disp.retryMsg.Delete(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ func (disp *Dispatcher) retry(messages []*RetryMessage) {
|
|||||||
|
|
||||||
// Schedule next retry with exponential backoff
|
// Schedule next retry with exponential backoff
|
||||||
msg.NextRetry = time.Now().Add(calculateBackoffDelay(msg.Trials))
|
msg.NextRetry = time.Now().Add(calculateBackoffDelay(msg.Trials))
|
||||||
disp.retryMsg.Add(msg)
|
disp.retryMsg.Store(msg, struct{}{})
|
||||||
|
|
||||||
log.Debug().EmbedObject(msg).Msg("notification retry failed, scheduled for later")
|
log.Debug().EmbedObject(msg).Msg("notification retry failed, scheduled for later")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/gotify/server/v2/model"
|
"github.com/gotify/server/v2/model"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -18,6 +19,16 @@ type (
|
|||||||
|
|
||||||
const gotifyMsgEndpoint = "/message"
|
const gotifyMsgEndpoint = "/message"
|
||||||
|
|
||||||
|
func (client *GotifyClient) Validate() gperr.Error {
|
||||||
|
if err := client.ProviderBase.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if client.Token == "" {
|
||||||
|
return gperr.New("token is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (client *GotifyClient) GetURL() string {
|
func (client *GotifyClient) GetURL() string {
|
||||||
return client.URL + gotifyMsgEndpoint
|
return client.URL + gotifyMsgEndpoint
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package route
|
package route
|
||||||
|
|
||||||
|
import route "github.com/yusing/godoxy/internal/route/types"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ImageNamePortMapTCP = map[string]int{
|
ImageNamePortMapTCP = map[string]int{
|
||||||
"mssql": 1433,
|
"mssql": 1433,
|
||||||
@@ -57,25 +59,25 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func getSchemePortByImageName(imageName string) (scheme string, port int, ok bool) {
|
func getSchemePortByImageName(imageName string) (scheme route.Scheme, port int, ok bool) {
|
||||||
if port, ok := ImageNamePortMapHTTP[imageName]; ok {
|
if port, ok := ImageNamePortMapHTTP[imageName]; ok {
|
||||||
return "http", port, true
|
return route.SchemeHTTP, port, true
|
||||||
}
|
}
|
||||||
if port, ok := ImageNamePortMapHTTPS[imageName]; ok {
|
if port, ok := ImageNamePortMapHTTPS[imageName]; ok {
|
||||||
return "https", port, true
|
return route.SchemeHTTPS, port, true
|
||||||
}
|
}
|
||||||
if port, ok := ImageNamePortMapTCP[imageName]; ok {
|
if port, ok := ImageNamePortMapTCP[imageName]; ok {
|
||||||
return "tcp", port, true
|
return route.SchemeTCP, port, true
|
||||||
}
|
}
|
||||||
return scheme, port, ok
|
return scheme, port, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSchemePortByAlias(alias string) (scheme string, port int, ok bool) {
|
func getSchemePortByAlias(alias string) (scheme route.Scheme, port int, ok bool) {
|
||||||
if port, ok := AliasPortMapHTTP[alias]; ok {
|
if port, ok := AliasPortMapHTTP[alias]; ok {
|
||||||
return "http", port, true
|
return route.SchemeHTTP, port, true
|
||||||
}
|
}
|
||||||
if port, ok := AliasPortMapHTTPS[alias]; ok {
|
if port, ok := AliasPortMapHTTPS[alias]; ok {
|
||||||
return "https", port, true
|
return route.SchemeHTTPS, port, true
|
||||||
}
|
}
|
||||||
return scheme, port, ok
|
return scheme, port, ok
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
D "github.com/yusing/godoxy/internal/docker"
|
D "github.com/yusing/godoxy/internal/docker"
|
||||||
"github.com/yusing/godoxy/internal/route"
|
"github.com/yusing/godoxy/internal/route"
|
||||||
T "github.com/yusing/godoxy/internal/route/types"
|
routeTypes "github.com/yusing/godoxy/internal/route/types"
|
||||||
expect "github.com/yusing/goutils/testing"
|
expect "github.com/yusing/goutils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,8 +91,8 @@ func TestApplyLabel(t *testing.T) {
|
|||||||
b, ok := entries["b"]
|
b, ok := entries["b"]
|
||||||
expect.True(t, ok)
|
expect.True(t, ok)
|
||||||
|
|
||||||
expect.Equal(t, a.Scheme, "https")
|
expect.Equal(t, a.Scheme, routeTypes.SchemeHTTPS)
|
||||||
expect.Equal(t, b.Scheme, "https")
|
expect.Equal(t, b.Scheme, routeTypes.SchemeHTTPS)
|
||||||
|
|
||||||
expect.Equal(t, a.Host, "app")
|
expect.Equal(t, a.Host, "app")
|
||||||
expect.Equal(t, b.Host, "app")
|
expect.Equal(t, b.Host, "app")
|
||||||
@@ -152,12 +152,12 @@ func TestApplyLabelWithAlias(t *testing.T) {
|
|||||||
c, ok := entries["c"]
|
c, ok := entries["c"]
|
||||||
expect.True(t, ok)
|
expect.True(t, ok)
|
||||||
|
|
||||||
expect.Equal(t, a.Scheme, "http")
|
expect.Equal(t, a.Scheme, routeTypes.SchemeHTTP)
|
||||||
expect.Equal(t, a.Port.Proxy, 3333)
|
expect.Equal(t, a.Port.Proxy, 3333)
|
||||||
expect.Equal(t, a.NoTLSVerify, true)
|
expect.Equal(t, a.NoTLSVerify, true)
|
||||||
expect.Equal(t, b.Scheme, "http")
|
expect.Equal(t, b.Scheme, routeTypes.SchemeHTTP)
|
||||||
expect.Equal(t, b.Port.Proxy, 1234)
|
expect.Equal(t, b.Port.Proxy, 1234)
|
||||||
expect.Equal(t, c.Scheme, "https")
|
expect.Equal(t, c.Scheme, routeTypes.SchemeHTTPS)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApplyLabelWithRef(t *testing.T) {
|
func TestApplyLabelWithRef(t *testing.T) {
|
||||||
@@ -180,11 +180,11 @@ func TestApplyLabelWithRef(t *testing.T) {
|
|||||||
c, ok := entries["c"]
|
c, ok := entries["c"]
|
||||||
expect.True(t, ok)
|
expect.True(t, ok)
|
||||||
|
|
||||||
expect.Equal(t, a.Scheme, "http")
|
expect.Equal(t, a.Scheme, routeTypes.SchemeHTTP)
|
||||||
expect.Equal(t, a.Host, "localhost")
|
expect.Equal(t, a.Host, "localhost")
|
||||||
expect.Equal(t, a.Port.Proxy, 4444)
|
expect.Equal(t, a.Port.Proxy, 4444)
|
||||||
expect.Equal(t, b.Port.Proxy, 9999)
|
expect.Equal(t, b.Port.Proxy, 9999)
|
||||||
expect.Equal(t, c.Scheme, "https")
|
expect.Equal(t, c.Scheme, routeTypes.SchemeHTTPS)
|
||||||
expect.Equal(t, c.Port.Proxy, 1111)
|
expect.Equal(t, c.Port.Proxy, 1111)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,12 +229,12 @@ func TestDynamicAliases(t *testing.T) {
|
|||||||
|
|
||||||
r, ok := entries["app1"]
|
r, ok := entries["app1"]
|
||||||
expect.True(t, ok)
|
expect.True(t, ok)
|
||||||
expect.Equal(t, r.Scheme, "http")
|
expect.Equal(t, r.Scheme, routeTypes.SchemeHTTP)
|
||||||
expect.Equal(t, r.Port.Proxy, 1234)
|
expect.Equal(t, r.Port.Proxy, 1234)
|
||||||
|
|
||||||
r, ok = entries["app1_backend"]
|
r, ok = entries["app1_backend"]
|
||||||
expect.True(t, ok)
|
expect.True(t, ok)
|
||||||
expect.Equal(t, r.Scheme, "http")
|
expect.Equal(t, r.Scheme, routeTypes.SchemeHTTP)
|
||||||
expect.Equal(t, r.Port.Proxy, 5678)
|
expect.Equal(t, r.Port.Proxy, 5678)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +327,7 @@ func TestStreamDefaultValues(t *testing.T) {
|
|||||||
r, ok := makeRoutes(cont)["a"]
|
r, ok := makeRoutes(cont)["a"]
|
||||||
expect.True(t, ok)
|
expect.True(t, ok)
|
||||||
expect.NoError(t, r.Validate())
|
expect.NoError(t, r.Validate())
|
||||||
expect.Equal(t, r.Scheme, T.Scheme("udp"))
|
expect.Equal(t, r.Scheme, routeTypes.SchemeUDP)
|
||||||
expect.Equal(t, r.TargetURL().Hostname(), privIP)
|
expect.Equal(t, r.TargetURL().Hostname(), privIP)
|
||||||
expect.Equal(t, r.Port.Listening, 0)
|
expect.Equal(t, r.Port.Listening, 0)
|
||||||
expect.Equal(t, r.Port.Proxy, int(privPort))
|
expect.Equal(t, r.Port.Proxy, int(privPort))
|
||||||
@@ -337,7 +337,7 @@ func TestStreamDefaultValues(t *testing.T) {
|
|||||||
r, ok := makeRoutes(cont, testIP)["a"]
|
r, ok := makeRoutes(cont, testIP)["a"]
|
||||||
expect.True(t, ok)
|
expect.True(t, ok)
|
||||||
expect.NoError(t, r.Validate())
|
expect.NoError(t, r.Validate())
|
||||||
expect.Equal(t, r.Scheme, T.Scheme("udp"))
|
expect.Equal(t, r.Scheme, routeTypes.SchemeUDP)
|
||||||
expect.Equal(t, r.TargetURL().Hostname(), testIP)
|
expect.Equal(t, r.TargetURL().Hostname(), testIP)
|
||||||
expect.Equal(t, r.Port.Listening, 0)
|
expect.Equal(t, r.Port.Listening, 0)
|
||||||
expect.Equal(t, r.Port.Proxy, int(pubPort))
|
expect.Equal(t, r.Port.Proxy, int(pubPort))
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(r.Rules) > 0 {
|
if len(r.Rules) > 0 {
|
||||||
r.handler = r.Rules.BuildHandler(r.handler)
|
r.handler = r.Rules.BuildHandler(r.handler.ServeHTTP)
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.HealthMon != nil {
|
if r.HealthMon != nil {
|
||||||
|
|||||||
@@ -2,8 +2,13 @@ package route
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,6 +22,7 @@ import (
|
|||||||
netutils "github.com/yusing/godoxy/internal/net"
|
netutils "github.com/yusing/godoxy/internal/net"
|
||||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||||
"github.com/yusing/godoxy/internal/proxmox"
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
@@ -25,6 +31,7 @@ import (
|
|||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||||
"github.com/yusing/godoxy/internal/route/rules"
|
"github.com/yusing/godoxy/internal/route/rules"
|
||||||
|
rulepresets "github.com/yusing/godoxy/internal/route/rules/presets"
|
||||||
route "github.com/yusing/godoxy/internal/route/types"
|
route "github.com/yusing/godoxy/internal/route/types"
|
||||||
"github.com/yusing/godoxy/internal/utils"
|
"github.com/yusing/godoxy/internal/utils"
|
||||||
)
|
)
|
||||||
@@ -34,14 +41,15 @@ type (
|
|||||||
_ utils.NoCopy
|
_ utils.NoCopy
|
||||||
|
|
||||||
Alias string `json:"alias"`
|
Alias string `json:"alias"`
|
||||||
Scheme route.Scheme `json:"scheme,omitempty"`
|
Scheme route.Scheme `json:"scheme,omitempty" swaggertype:"string" enums:"http,https,tcp,udp,fileserver"`
|
||||||
Host string `json:"host,omitempty"`
|
Host string `json:"host,omitempty"`
|
||||||
Port route.Port `json:"port"`
|
Port route.Port `json:"port"`
|
||||||
Root string `json:"root,omitempty"`
|
Root string `json:"root,omitempty"`
|
||||||
|
|
||||||
route.HTTPConfig
|
route.HTTPConfig
|
||||||
PathPatterns []string `json:"path_patterns,omitempty" extensions:"x-nullable"`
|
PathPatterns []string `json:"path_patterns,omitempty" extensions:"x-nullable"`
|
||||||
Rules rules.Rules `json:"rules,omitempty" validate:"omitempty,unique=Name" extension:"x-nullable"`
|
Rules rules.Rules `json:"rules,omitempty" extension:"x-nullable"`
|
||||||
|
RuleFile string `json:"rule_file,omitempty" extensions:"x-nullable"`
|
||||||
HealthCheck *types.HealthCheckConfig `json:"healthcheck"`
|
HealthCheck *types.HealthCheckConfig `json:"healthcheck"`
|
||||||
LoadBalance *types.LoadBalancerConfig `json:"load_balance,omitempty" extensions:"x-nullable"`
|
LoadBalance *types.LoadBalancerConfig `json:"load_balance,omitempty" extensions:"x-nullable"`
|
||||||
Middlewares map[string]types.LabelMap `json:"middlewares,omitempty" extensions:"x-nullable"`
|
Middlewares map[string]types.LabelMap `json:"middlewares,omitempty" extensions:"x-nullable"`
|
||||||
@@ -64,8 +72,8 @@ type (
|
|||||||
LisURL *nettypes.URL `json:"lurl,omitempty" swaggertype:"string" extensions:"x-nullable"`
|
LisURL *nettypes.URL `json:"lurl,omitempty" swaggertype:"string" extensions:"x-nullable"`
|
||||||
ProxyURL *nettypes.URL `json:"purl,omitempty" swaggertype:"string"`
|
ProxyURL *nettypes.URL `json:"purl,omitempty" swaggertype:"string"`
|
||||||
|
|
||||||
Excluded bool `json:"excluded,omitempty" extensions:"x-nullable"`
|
Excluded bool `json:"excluded,omitempty" extensions:"x-nullable"`
|
||||||
ExcludedReason string `json:"excluded_reason,omitempty" extensions:"x-nullable"`
|
ExcludedReason ExcludedReason `json:"excluded_reason,omitempty" swaggertype:"string" extensions:"x-nullable"`
|
||||||
|
|
||||||
HealthMon types.HealthMonitor `json:"health,omitempty" swaggerignore:"true"`
|
HealthMon types.HealthMonitor `json:"health,omitempty" swaggerignore:"true"`
|
||||||
// for swagger
|
// for swagger
|
||||||
@@ -212,7 +220,10 @@ func (r *Route) Validate() gperr.Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := gperr.NewBuilder("entry validation failed")
|
var errs gperr.Builder
|
||||||
|
if err := r.validateRules(); err != nil {
|
||||||
|
errs.Add(err)
|
||||||
|
}
|
||||||
|
|
||||||
var impl types.Route
|
var impl types.Route
|
||||||
var err gperr.Error
|
var err gperr.Error
|
||||||
@@ -262,7 +273,40 @@ func (r *Route) Validate() gperr.Error {
|
|||||||
r.impl = impl
|
r.impl = impl
|
||||||
r.Excluded = r.ShouldExclude()
|
r.Excluded = r.ShouldExclude()
|
||||||
if r.Excluded {
|
if r.Excluded {
|
||||||
r.ExcludedReason = r.GetExcludedReason()
|
r.ExcludedReason = r.findExcludedReason()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Route) validateRules() error {
|
||||||
|
if r.RuleFile != "" && len(r.Rules) > 0 {
|
||||||
|
return errors.New("`rule_file` and `rules` cannot be used together")
|
||||||
|
} else if r.RuleFile != "" {
|
||||||
|
src, err := url.Parse(r.RuleFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse rule file url %q: %w", r.RuleFile, err)
|
||||||
|
}
|
||||||
|
switch src.Scheme {
|
||||||
|
case "embed": // embed://<preset_file_name>
|
||||||
|
rules, ok := rulepresets.GetRulePreset(src.Host)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("rule preset %q not found", src.Host)
|
||||||
|
} else {
|
||||||
|
r.Rules = rules
|
||||||
|
}
|
||||||
|
case "file", "":
|
||||||
|
content, err := os.ReadFile(src.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read rule file %q: %w", src.Path, err)
|
||||||
|
} else {
|
||||||
|
_, err = serialization.ConvertString(string(content), reflect.ValueOf(&r.Rules))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal rule file %q: %w", src.Path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported rule file scheme %q", src.Scheme)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -430,6 +474,9 @@ func (r *Route) HomepageItem() homepage.Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Route) DisplayName() string {
|
func (r *Route) DisplayName() string {
|
||||||
|
if r.Homepage == nil { // should only happen in tests, Validate() should initialize it
|
||||||
|
return r.Alias
|
||||||
|
}
|
||||||
return r.Homepage.Name
|
return r.Homepage.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,31 +522,73 @@ func (r *Route) ShouldExclude() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Route) GetExcludedReason() string {
|
type ExcludedReason uint8
|
||||||
if r.lastError != nil {
|
|
||||||
return string(gperr.Plain(r.lastError))
|
const (
|
||||||
|
ExcludedReasonNone ExcludedReason = iota
|
||||||
|
ExcludedReasonError
|
||||||
|
ExcludedReasonManual
|
||||||
|
ExcludedReasonNoPortContainer
|
||||||
|
ExcludedReasonNoPortSpecified
|
||||||
|
ExcludedReasonBlacklisted
|
||||||
|
ExcludedReasonBuildx
|
||||||
|
ExcludedReasonOld
|
||||||
|
)
|
||||||
|
|
||||||
|
func (re ExcludedReason) String() string {
|
||||||
|
switch re {
|
||||||
|
case ExcludedReasonNone:
|
||||||
|
return ""
|
||||||
|
case ExcludedReasonError:
|
||||||
|
return "Error"
|
||||||
|
case ExcludedReasonManual:
|
||||||
|
return "Manual exclusion"
|
||||||
|
case ExcludedReasonNoPortContainer:
|
||||||
|
return "No port exposed in container"
|
||||||
|
case ExcludedReasonNoPortSpecified:
|
||||||
|
return "No port specified"
|
||||||
|
case ExcludedReasonBlacklisted:
|
||||||
|
return "Blacklisted (backend service or database)"
|
||||||
|
case ExcludedReasonBuildx:
|
||||||
|
return "Buildx"
|
||||||
|
case ExcludedReasonOld:
|
||||||
|
return "Container renaming intermediate state"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
}
|
}
|
||||||
if r.ExcludedReason != "" {
|
}
|
||||||
|
|
||||||
|
func (re ExcludedReason) MarshalJSON() ([]byte, error) {
|
||||||
|
return strconv.AppendQuote(nil, re.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// no need to unmarshal json because we don't store this
|
||||||
|
|
||||||
|
func (r *Route) findExcludedReason() ExcludedReason {
|
||||||
|
if r.lastError != nil {
|
||||||
|
return ExcludedReasonError
|
||||||
|
}
|
||||||
|
if r.ExcludedReason != ExcludedReasonNone {
|
||||||
return r.ExcludedReason
|
return r.ExcludedReason
|
||||||
}
|
}
|
||||||
if r.Container != nil {
|
if r.Container != nil {
|
||||||
switch {
|
switch {
|
||||||
case r.Container.IsExcluded:
|
case r.Container.IsExcluded:
|
||||||
return "Manual exclusion"
|
return ExcludedReasonManual
|
||||||
case r.IsZeroPort() && !r.UseIdleWatcher():
|
case r.IsZeroPort() && !r.UseIdleWatcher():
|
||||||
return "No port exposed in container"
|
return ExcludedReasonNoPortContainer
|
||||||
case !r.Container.IsExplicit && docker.IsBlacklisted(r.Container):
|
case !r.Container.IsExplicit && docker.IsBlacklisted(r.Container):
|
||||||
return "Blacklisted (backend service or database)"
|
return ExcludedReasonBlacklisted
|
||||||
case strings.HasPrefix(r.Container.ContainerName, "buildx_"):
|
case strings.HasPrefix(r.Container.ContainerName, "buildx_"):
|
||||||
return "Buildx"
|
return ExcludedReasonBuildx
|
||||||
}
|
}
|
||||||
} else if r.IsZeroPort() && r.Scheme != route.SchemeFileServer {
|
} else if r.IsZeroPort() && r.Scheme != route.SchemeFileServer {
|
||||||
return "No port specified"
|
return ExcludedReasonNoPortSpecified
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(r.Alias, "-old") {
|
if strings.HasSuffix(r.Alias, "-old") {
|
||||||
return "Container renaming intermediate state"
|
return ExcludedReasonOld
|
||||||
}
|
}
|
||||||
return ""
|
return ExcludedReasonNone
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Route) UseLoadBalance() bool {
|
func (r *Route) UseLoadBalance() bool {
|
||||||
@@ -551,8 +640,8 @@ func (r *Route) Finalize() {
|
|||||||
if isDocker {
|
if isDocker {
|
||||||
scheme, port, ok := getSchemePortByImageName(cont.Image.Name)
|
scheme, port, ok := getSchemePortByImageName(cont.Image.Name)
|
||||||
if ok {
|
if ok {
|
||||||
if r.Scheme == "" {
|
if r.Scheme == route.SchemeNone {
|
||||||
r.Scheme = route.Scheme(scheme)
|
r.Scheme = scheme
|
||||||
}
|
}
|
||||||
if pp == 0 {
|
if pp == 0 {
|
||||||
pp = port
|
pp = port
|
||||||
@@ -561,8 +650,8 @@ func (r *Route) Finalize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if scheme, port, ok := getSchemePortByAlias(r.Alias); ok {
|
if scheme, port, ok := getSchemePortByAlias(r.Alias); ok {
|
||||||
if r.Scheme == "" {
|
if r.Scheme == route.SchemeNone {
|
||||||
r.Scheme = route.Scheme(scheme)
|
r.Scheme = scheme
|
||||||
}
|
}
|
||||||
if pp == 0 {
|
if pp == 0 {
|
||||||
pp = port
|
pp = port
|
||||||
@@ -577,7 +666,7 @@ func (r *Route) Finalize() {
|
|||||||
} else {
|
} else {
|
||||||
pp = preferredPort(cont.PrivatePortMapping)
|
pp = preferredPort(cont.PrivatePortMapping)
|
||||||
}
|
}
|
||||||
case r.Scheme == "https":
|
case r.Scheme == route.SchemeHTTPS:
|
||||||
pp = 443
|
pp = 443
|
||||||
default:
|
default:
|
||||||
pp = 80
|
pp = 80
|
||||||
@@ -585,10 +674,10 @@ func (r *Route) Finalize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isDocker {
|
if isDocker {
|
||||||
if r.Scheme == "" {
|
if r.Scheme == route.SchemeNone {
|
||||||
for _, p := range cont.PublicPortMapping {
|
for _, p := range cont.PublicPortMapping {
|
||||||
if int(p.PrivatePort) == pp && p.Type == "udp" {
|
if int(p.PrivatePort) == pp && p.Type == "udp" {
|
||||||
r.Scheme = "udp"
|
r.Scheme = route.SchemeUDP
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -606,14 +695,14 @@ func (r *Route) Finalize() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Scheme == "" {
|
if r.Scheme == route.SchemeNone {
|
||||||
switch {
|
switch {
|
||||||
case lp != 0:
|
case lp != 0:
|
||||||
r.Scheme = "tcp"
|
r.Scheme = route.SchemeTCP
|
||||||
case pp%1000 == 443:
|
case pp%1000 == 443:
|
||||||
r.Scheme = "https"
|
r.Scheme = route.SchemeHTTPS
|
||||||
default: // assume its http
|
default: // assume its http
|
||||||
r.Scheme = "http"
|
r.Scheme = route.SchemeHTTP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ func TestRouteValidate(t *testing.T) {
|
|||||||
t.Run("InvalidScheme", func(t *testing.T) {
|
t.Run("InvalidScheme", func(t *testing.T) {
|
||||||
r := &Route{
|
r := &Route{
|
||||||
Alias: "test",
|
Alias: "test",
|
||||||
Scheme: "invalid",
|
Scheme: 123,
|
||||||
Host: "example.com",
|
Host: "example.com",
|
||||||
Port: route.Port{Proxy: 80},
|
Port: route.Port{Proxy: 80},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,13 @@ func TryGetUpstreamPort(r *http.Request) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TryGetUpstreamHostPort(r *http.Request) string {
|
||||||
|
if u := tryGetURL(r); u != nil {
|
||||||
|
return u.Host
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func TryGetUpstreamAddr(r *http.Request) string {
|
func TryGetUpstreamAddr(r *http.Request) string {
|
||||||
if u := tryGetURL(r); u != nil {
|
if u := tryGetURL(r); u != nil {
|
||||||
return u.Host
|
return u.Host
|
||||||
|
|||||||
@@ -1,56 +1,21 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HealthInfo struct {
|
type HealthInfo struct {
|
||||||
|
HealthInfoWithoutDetail
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
} // @name HealthInfo
|
||||||
|
|
||||||
|
type HealthInfoWithoutDetail struct {
|
||||||
Status types.HealthStatus `json:"status" swaggertype:"string" enums:"healthy,unhealthy,napping,starting,error,unknown"`
|
Status types.HealthStatus `json:"status" swaggertype:"string" enums:"healthy,unhealthy,napping,starting,error,unknown"`
|
||||||
Uptime time.Duration `json:"uptime" swaggertype:"number"` // uptime in milliseconds
|
Uptime time.Duration `json:"uptime" swaggertype:"number"` // uptime in milliseconds
|
||||||
Latency time.Duration `json:"latency" swaggertype:"number"` // latency in microseconds
|
Latency time.Duration `json:"latency" swaggertype:"number"` // latency in microseconds
|
||||||
Detail string `json:"detail"`
|
} // @name HealthInfoWithoutDetail
|
||||||
}
|
|
||||||
|
|
||||||
func (info *HealthInfo) MarshalJSON() ([]byte, error) {
|
|
||||||
return sonic.Marshal(map[string]any{
|
|
||||||
"status": info.Status.String(),
|
|
||||||
"latency": info.Latency.Microseconds(),
|
|
||||||
"uptime": info.Uptime.Milliseconds(),
|
|
||||||
"detail": info.Detail,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (info *HealthInfo) UnmarshalJSON(data []byte) error {
|
|
||||||
var v struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Latency int64 `json:"latency"`
|
|
||||||
Uptime int64 `json:"uptime"`
|
|
||||||
Detail string `json:"detail"`
|
|
||||||
}
|
|
||||||
if err := sonic.Unmarshal(data, &v); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// overflow check
|
|
||||||
// Check if latency (in microseconds) would overflow when converted to nanoseconds
|
|
||||||
if v.Latency > math.MaxInt64/int64(time.Microsecond) {
|
|
||||||
v.Latency = 0
|
|
||||||
}
|
|
||||||
// Check if uptime (in milliseconds) would overflow when converted to nanoseconds
|
|
||||||
if v.Uptime > math.MaxInt64/int64(time.Millisecond) {
|
|
||||||
v.Uptime = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
info.Status = types.NewHealthStatusFromString(v.Status)
|
|
||||||
info.Latency = time.Duration(v.Latency) * time.Microsecond
|
|
||||||
info.Uptime = time.Duration(v.Uptime) * time.Millisecond
|
|
||||||
info.Detail = v.Detail
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetHealthInfo() map[string]HealthInfo {
|
func GetHealthInfo() map[string]HealthInfo {
|
||||||
healthMap := make(map[string]HealthInfo, NumRoutes())
|
healthMap := make(map[string]HealthInfo, NumRoutes())
|
||||||
@@ -60,19 +25,45 @@ func GetHealthInfo() map[string]HealthInfo {
|
|||||||
return healthMap
|
return healthMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetHealthInfoWithoutDetail() map[string]HealthInfoWithoutDetail {
|
||||||
|
healthMap := make(map[string]HealthInfoWithoutDetail, NumRoutes())
|
||||||
|
for r := range Iter {
|
||||||
|
healthMap[r.Name()] = getHealthInfoWithoutDetail(r)
|
||||||
|
}
|
||||||
|
return healthMap
|
||||||
|
}
|
||||||
|
|
||||||
func getHealthInfo(r types.Route) HealthInfo {
|
func getHealthInfo(r types.Route) HealthInfo {
|
||||||
mon := r.HealthMonitor()
|
mon := r.HealthMonitor()
|
||||||
if mon == nil {
|
if mon == nil {
|
||||||
return HealthInfo{
|
return HealthInfo{
|
||||||
Status: types.StatusUnknown,
|
HealthInfoWithoutDetail: HealthInfoWithoutDetail{
|
||||||
|
Status: types.StatusUnknown,
|
||||||
|
},
|
||||||
Detail: "n/a",
|
Detail: "n/a",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return HealthInfo{
|
return HealthInfo{
|
||||||
|
HealthInfoWithoutDetail: HealthInfoWithoutDetail{
|
||||||
|
Status: mon.Status(),
|
||||||
|
Uptime: mon.Uptime(),
|
||||||
|
Latency: mon.Latency(),
|
||||||
|
},
|
||||||
|
Detail: mon.Detail(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHealthInfoWithoutDetail(r types.Route) HealthInfoWithoutDetail {
|
||||||
|
mon := r.HealthMonitor()
|
||||||
|
if mon == nil {
|
||||||
|
return HealthInfoWithoutDetail{
|
||||||
|
Status: types.StatusUnknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return HealthInfoWithoutDetail{
|
||||||
Status: mon.Status(),
|
Status: mon.Status(),
|
||||||
Uptime: mon.Uptime(),
|
Uptime: mon.Uptime(),
|
||||||
Latency: mon.Latency(),
|
Latency: mon.Latency(),
|
||||||
Detail: mon.Detail(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CacheKeyQueries = "queries"
|
cacheKeyQueries = "queries"
|
||||||
CacheKeyCookies = "cookies"
|
cacheKeyCookies = "cookies"
|
||||||
CacheKeyRemoteIP = "remote_ip"
|
cacheKeyRemoteIP = "remote_ip"
|
||||||
CacheKeyBasicAuth = "basic_auth"
|
cacheKeyBasicAuth = "basic_auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cachePool = &sync.Pool{
|
var cachePool = sync.Pool{
|
||||||
New: func() any {
|
New: func() any {
|
||||||
return make(Cache)
|
return make(Cache)
|
||||||
},
|
},
|
||||||
@@ -41,10 +41,10 @@ func (c Cache) Release() {
|
|||||||
// GetQueries returns the queries.
|
// GetQueries returns the queries.
|
||||||
// If r does not have queries, an empty map is returned.
|
// If r does not have queries, an empty map is returned.
|
||||||
func (c Cache) GetQueries(r *http.Request) url.Values {
|
func (c Cache) GetQueries(r *http.Request) url.Values {
|
||||||
v, ok := c[CacheKeyQueries]
|
v, ok := c[cacheKeyQueries]
|
||||||
if !ok {
|
if !ok {
|
||||||
v = r.URL.Query()
|
v = r.URL.Query()
|
||||||
c[CacheKeyQueries] = v
|
c[cacheKeyQueries] = v
|
||||||
}
|
}
|
||||||
return v.(url.Values)
|
return v.(url.Values)
|
||||||
}
|
}
|
||||||
@@ -58,17 +58,17 @@ func (c Cache) UpdateQueries(r *http.Request, update func(url.Values)) {
|
|||||||
// GetCookies returns the cookies.
|
// GetCookies returns the cookies.
|
||||||
// If r does not have cookies, an empty slice is returned.
|
// If r does not have cookies, an empty slice is returned.
|
||||||
func (c Cache) GetCookies(r *http.Request) []*http.Cookie {
|
func (c Cache) GetCookies(r *http.Request) []*http.Cookie {
|
||||||
v, ok := c[CacheKeyCookies]
|
v, ok := c[cacheKeyCookies]
|
||||||
if !ok {
|
if !ok {
|
||||||
v = r.Cookies()
|
v = r.Cookies()
|
||||||
c[CacheKeyCookies] = v
|
c[cacheKeyCookies] = v
|
||||||
}
|
}
|
||||||
return v.([]*http.Cookie)
|
return v.([]*http.Cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Cache) UpdateCookies(r *http.Request, update UpdateFunc[[]*http.Cookie]) {
|
func (c Cache) UpdateCookies(r *http.Request, update UpdateFunc[[]*http.Cookie]) {
|
||||||
cookies := update(c.GetCookies(r))
|
cookies := update(c.GetCookies(r))
|
||||||
c[CacheKeyCookies] = cookies
|
c[cacheKeyCookies] = cookies
|
||||||
r.Header.Del("Cookie")
|
r.Header.Del("Cookie")
|
||||||
for _, cookie := range cookies {
|
for _, cookie := range cookies {
|
||||||
r.AddCookie(cookie)
|
r.AddCookie(cookie)
|
||||||
@@ -78,14 +78,14 @@ func (c Cache) UpdateCookies(r *http.Request, update UpdateFunc[[]*http.Cookie])
|
|||||||
// GetRemoteIP returns the remote ip address.
|
// GetRemoteIP returns the remote ip address.
|
||||||
// If r.RemoteAddr is not a valid ip address, nil is returned.
|
// If r.RemoteAddr is not a valid ip address, nil is returned.
|
||||||
func (c Cache) GetRemoteIP(r *http.Request) net.IP {
|
func (c Cache) GetRemoteIP(r *http.Request) net.IP {
|
||||||
v, ok := c[CacheKeyRemoteIP]
|
v, ok := c[cacheKeyRemoteIP]
|
||||||
if !ok {
|
if !ok {
|
||||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host = r.RemoteAddr
|
host = r.RemoteAddr
|
||||||
}
|
}
|
||||||
v = net.ParseIP(host)
|
v = net.ParseIP(host)
|
||||||
c[CacheKeyRemoteIP] = v
|
c[cacheKeyRemoteIP] = v
|
||||||
}
|
}
|
||||||
return v.(net.IP)
|
return v.(net.IP)
|
||||||
}
|
}
|
||||||
@@ -93,14 +93,14 @@ func (c Cache) GetRemoteIP(r *http.Request) net.IP {
|
|||||||
// GetBasicAuth returns *Credentials the basic auth username and password.
|
// GetBasicAuth returns *Credentials the basic auth username and password.
|
||||||
// If r does not have basic auth, nil is returned.
|
// If r does not have basic auth, nil is returned.
|
||||||
func (c Cache) GetBasicAuth(r *http.Request) *Credentials {
|
func (c Cache) GetBasicAuth(r *http.Request) *Credentials {
|
||||||
v, ok := c[CacheKeyBasicAuth]
|
v, ok := c[cacheKeyBasicAuth]
|
||||||
if !ok {
|
if !ok {
|
||||||
u, p, ok := r.BasicAuth()
|
u, p, ok := r.BasicAuth()
|
||||||
if ok {
|
if ok {
|
||||||
v = &Credentials{u, []byte(p)}
|
v = &Credentials{u, []byte(p)}
|
||||||
c[CacheKeyBasicAuth] = v
|
c[cacheKeyBasicAuth] = v
|
||||||
} else {
|
} else {
|
||||||
c[CacheKeyBasicAuth] = nil
|
c[cacheKeyBasicAuth] = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,30 +3,30 @@ package rules
|
|||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
type (
|
type (
|
||||||
CheckFunc func(cached Cache, r *http.Request) bool
|
CheckFunc func(w http.ResponseWriter, r *http.Request) bool
|
||||||
Checker interface {
|
Checker interface {
|
||||||
Check(cached Cache, r *http.Request) bool
|
Check(w http.ResponseWriter, r *http.Request) bool
|
||||||
}
|
}
|
||||||
CheckMatchSingle []Checker
|
CheckMatchSingle []Checker
|
||||||
CheckMatchAll []Checker
|
CheckMatchAll []Checker
|
||||||
)
|
)
|
||||||
|
|
||||||
func (checker CheckFunc) Check(cached Cache, r *http.Request) bool {
|
func (checker CheckFunc) Check(w http.ResponseWriter, r *http.Request) bool {
|
||||||
return checker(cached, r)
|
return checker(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (checkers CheckMatchSingle) Check(cached Cache, r *http.Request) bool {
|
func (checkers CheckMatchSingle) Check(w http.ResponseWriter, r *http.Request) bool {
|
||||||
for _, check := range checkers {
|
for _, check := range checkers {
|
||||||
if check.Check(cached, r) {
|
if check.Check(w, r) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (checkers CheckMatchAll) Check(cached Cache, r *http.Request) bool {
|
func (checkers CheckMatchAll) Check(w http.ResponseWriter, r *http.Request) bool {
|
||||||
for _, check := range checkers {
|
for _, check := range checkers {
|
||||||
if !check.Check(cached, r) {
|
if !check.Check(w, r) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,21 @@ package rules
|
|||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
handlerFunc func(w http.ResponseWriter, r *http.Request) error
|
||||||
|
|
||||||
CommandHandler interface {
|
CommandHandler interface {
|
||||||
// CommandHandler can read and modify the values
|
// CommandHandler can read and modify the values
|
||||||
// then handle the request
|
// then handle the request
|
||||||
// finally proceed to next command (or return) base on situation
|
// finally proceed to next command (or return) base on situation
|
||||||
Handle(cached Cache, w http.ResponseWriter, r *http.Request) (proceed bool)
|
Handle(w http.ResponseWriter, r *http.Request) error
|
||||||
|
IsResponseHandler() bool
|
||||||
}
|
}
|
||||||
// NonTerminatingCommand will run then proceed to next command or reverse proxy.
|
// NonTerminatingCommand will run then proceed to next command or reverse proxy.
|
||||||
NonTerminatingCommand http.HandlerFunc
|
NonTerminatingCommand handlerFunc
|
||||||
// TerminatingCommand will run then return immediately.
|
// TerminatingCommand will run then return immediately.
|
||||||
TerminatingCommand http.HandlerFunc
|
TerminatingCommand handlerFunc
|
||||||
// DynamicCommand will return base on the request
|
// OnResponseCommand will run then return based on the response.
|
||||||
// and can read or modify the values.
|
OnResponseCommand handlerFunc
|
||||||
DynamicCommand func(cached Cache, w http.ResponseWriter, r *http.Request) (proceed bool)
|
|
||||||
// BypassCommand will skip all the following commands
|
// BypassCommand will skip all the following commands
|
||||||
// and directly return to reverse proxy.
|
// and directly return to reverse proxy.
|
||||||
BypassCommand struct{}
|
BypassCommand struct{}
|
||||||
@@ -23,29 +25,55 @@ type (
|
|||||||
Commands []CommandHandler
|
Commands []CommandHandler
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c NonTerminatingCommand) Handle(cached Cache, w http.ResponseWriter, r *http.Request) (proceed bool) {
|
func (c NonTerminatingCommand) Handle(w http.ResponseWriter, r *http.Request) error {
|
||||||
c(w, r)
|
return c(w, r)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c TerminatingCommand) Handle(cached Cache, w http.ResponseWriter, r *http.Request) (proceed bool) {
|
func (c NonTerminatingCommand) IsResponseHandler() bool {
|
||||||
c(w, r)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c DynamicCommand) Handle(cached Cache, w http.ResponseWriter, r *http.Request) (proceed bool) {
|
func (c TerminatingCommand) Handle(w http.ResponseWriter, r *http.Request) error {
|
||||||
return c(cached, w, r)
|
if err := c(w, r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return errTerminated
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c BypassCommand) Handle(cached Cache, w http.ResponseWriter, r *http.Request) (proceed bool) {
|
func (c TerminatingCommand) IsResponseHandler() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c OnResponseCommand) Handle(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
return c(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c OnResponseCommand) IsResponseHandler() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Commands) Handle(cached Cache, w http.ResponseWriter, r *http.Request) (proceed bool) {
|
func (c BypassCommand) Handle(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
return errTerminated
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c BypassCommand) IsResponseHandler() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Commands) Handle(w http.ResponseWriter, r *http.Request) error {
|
||||||
for _, cmd := range c {
|
for _, cmd := range c {
|
||||||
if !cmd.Handle(cached, w, r) {
|
if err := cmd.Handle(w, r); err != nil {
|
||||||
return false
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Commands) IsResponseHandler() bool {
|
||||||
|
for _, cmd := range c {
|
||||||
|
if cmd.IsResponseHandler() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,40 @@
|
|||||||
package rules
|
package rules
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/yusing/godoxy/internal/auth"
|
||||||
|
"github.com/yusing/godoxy/internal/logging"
|
||||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||||
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
httputils "github.com/yusing/goutils/http"
|
httputils "github.com/yusing/goutils/http"
|
||||||
"github.com/yusing/goutils/http/reverseproxy"
|
"github.com/yusing/goutils/http/reverseproxy"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Command struct {
|
Command struct {
|
||||||
raw string
|
raw string
|
||||||
exec CommandHandler
|
exec CommandHandler
|
||||||
|
isResponseHandler bool
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (cmd *Command) IsResponseHandler() bool {
|
||||||
|
return cmd.isResponseHandler
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
CommandRequireAuth = "require_auth"
|
||||||
CommandRewrite = "rewrite"
|
CommandRewrite = "rewrite"
|
||||||
CommandServe = "serve"
|
CommandServe = "serve"
|
||||||
CommandProxy = "proxy"
|
CommandProxy = "proxy"
|
||||||
@@ -31,18 +44,46 @@ const (
|
|||||||
CommandSet = "set"
|
CommandSet = "set"
|
||||||
CommandAdd = "add"
|
CommandAdd = "add"
|
||||||
CommandRemove = "remove"
|
CommandRemove = "remove"
|
||||||
|
CommandLog = "log"
|
||||||
|
CommandNotify = "notify"
|
||||||
CommandPass = "pass"
|
CommandPass = "pass"
|
||||||
CommandPassAlt = "bypass"
|
CommandPassAlt = "bypass"
|
||||||
)
|
)
|
||||||
|
|
||||||
var commands = map[string]struct {
|
var commands = map[string]struct {
|
||||||
help Help
|
help Help
|
||||||
validate ValidateFunc
|
validate ValidateFunc
|
||||||
build func(args any) CommandHandler
|
build func(args any) CommandHandler
|
||||||
|
isResponseHandler bool
|
||||||
}{
|
}{
|
||||||
|
CommandRequireAuth: {
|
||||||
|
help: Help{
|
||||||
|
command: CommandRequireAuth,
|
||||||
|
description: makeLines("Require HTTP authentication for incoming requests"),
|
||||||
|
args: map[string]string{},
|
||||||
|
},
|
||||||
|
validate: func(args []string) (any, gperr.Error) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, ErrExpectNoArg
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
build: func(args any) CommandHandler {
|
||||||
|
return NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
if !auth.AuthOrProceed(w, r) {
|
||||||
|
return errTerminated
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
CommandRewrite: {
|
CommandRewrite: {
|
||||||
help: Help{
|
help: Help{
|
||||||
command: CommandRewrite,
|
command: CommandRewrite,
|
||||||
|
description: makeLines(
|
||||||
|
"Rewrite a request path from one prefix to another, e.g.:",
|
||||||
|
helpExample(CommandRewrite, "/foo", "/bar"),
|
||||||
|
),
|
||||||
args: map[string]string{
|
args: map[string]string{
|
||||||
"from": "the path to rewrite, must start with /",
|
"from": "the path to rewrite, must start with /",
|
||||||
"to": "the path to rewrite to, must start with /",
|
"to": "the path to rewrite to, must start with /",
|
||||||
@@ -67,24 +108,29 @@ var commands = map[string]struct {
|
|||||||
},
|
},
|
||||||
build: func(args any) CommandHandler {
|
build: func(args any) CommandHandler {
|
||||||
orig, repl := args.(*StrTuple).Unpack()
|
orig, repl := args.(*StrTuple).Unpack()
|
||||||
return NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) {
|
return NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
if len(path) > 0 && path[0] != '/' {
|
if len(path) > 0 && path[0] != '/' {
|
||||||
path = "/" + path
|
path = "/" + path
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(path, orig) {
|
if !strings.HasPrefix(path, orig) {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
path = repl + path[len(orig):]
|
path = repl + path[len(orig):]
|
||||||
r.URL.Path = path
|
r.URL.Path = path
|
||||||
r.URL.RawPath = r.URL.EscapedPath()
|
r.URL.RawPath = r.URL.EscapedPath()
|
||||||
r.RequestURI = r.URL.RequestURI()
|
r.RequestURI = r.URL.RequestURI()
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CommandServe: {
|
CommandServe: {
|
||||||
help: Help{
|
help: Help{
|
||||||
command: CommandServe,
|
command: CommandServe,
|
||||||
|
description: makeLines(
|
||||||
|
"Serve static files from a local file system path, e.g.:",
|
||||||
|
helpExample(CommandServe, "/var/www"),
|
||||||
|
),
|
||||||
args: map[string]string{
|
args: map[string]string{
|
||||||
"root": "the file system path to serve, must be an existing directory",
|
"root": "the file system path to serve, must be an existing directory",
|
||||||
},
|
},
|
||||||
@@ -92,14 +138,19 @@ var commands = map[string]struct {
|
|||||||
validate: validateFSPath,
|
validate: validateFSPath,
|
||||||
build: func(args any) CommandHandler {
|
build: func(args any) CommandHandler {
|
||||||
root := args.(string)
|
root := args.(string)
|
||||||
return TerminatingCommand(func(w http.ResponseWriter, r *http.Request) {
|
return TerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
http.ServeFile(w, r, path.Join(root, path.Clean(r.URL.Path)))
|
http.ServeFile(w, r, path.Join(root, path.Clean(r.URL.Path)))
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CommandRedirect: {
|
CommandRedirect: {
|
||||||
help: Help{
|
help: Help{
|
||||||
command: CommandRedirect,
|
command: CommandRedirect,
|
||||||
|
description: makeLines(
|
||||||
|
"Redirect request to another URL, e.g.:",
|
||||||
|
helpExample(CommandRedirect, "https://example.com"),
|
||||||
|
),
|
||||||
args: map[string]string{
|
args: map[string]string{
|
||||||
"to": "the url to redirect to, can be relative or absolute URL",
|
"to": "the url to redirect to, can be relative or absolute URL",
|
||||||
},
|
},
|
||||||
@@ -107,14 +158,19 @@ var commands = map[string]struct {
|
|||||||
validate: validateURL,
|
validate: validateURL,
|
||||||
build: func(args any) CommandHandler {
|
build: func(args any) CommandHandler {
|
||||||
target := args.(*nettypes.URL).String()
|
target := args.(*nettypes.URL).String()
|
||||||
return TerminatingCommand(func(w http.ResponseWriter, r *http.Request) {
|
return TerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
http.Redirect(w, r, target, http.StatusTemporaryRedirect)
|
http.Redirect(w, r, target, http.StatusTemporaryRedirect)
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CommandError: {
|
CommandError: {
|
||||||
help: Help{
|
help: Help{
|
||||||
command: CommandError,
|
command: CommandError,
|
||||||
|
description: makeLines(
|
||||||
|
"Send an HTTP error response and terminate processing, e.g.:",
|
||||||
|
helpExample(CommandError, "400", "bad request"),
|
||||||
|
),
|
||||||
args: map[string]string{
|
args: map[string]string{
|
||||||
"code": "the http status code to return",
|
"code": "the http status code to return",
|
||||||
"text": "the error message to return",
|
"text": "the error message to return",
|
||||||
@@ -132,18 +188,30 @@ var commands = map[string]struct {
|
|||||||
if !httputils.IsStatusCodeValid(code) {
|
if !httputils.IsStatusCodeValid(code) {
|
||||||
return nil, ErrInvalidArguments.Subject(codeStr)
|
return nil, ErrInvalidArguments.Subject(codeStr)
|
||||||
}
|
}
|
||||||
return &Tuple[int, string]{code, text}, nil
|
textTmpl, err := validateTemplate(text, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrInvalidArguments.With(err)
|
||||||
|
}
|
||||||
|
return &Tuple[int, templateString]{code, textTmpl}, nil
|
||||||
},
|
},
|
||||||
build: func(args any) CommandHandler {
|
build: func(args any) CommandHandler {
|
||||||
code, text := args.(*Tuple[int, string]).Unpack()
|
code, textTmpl := args.(*Tuple[int, templateString]).Unpack()
|
||||||
return TerminatingCommand(func(w http.ResponseWriter, r *http.Request) {
|
return TerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
http.Error(w, text, code)
|
// error command should overwrite the response body
|
||||||
|
GetInitResponseModifier(w).ResetBody()
|
||||||
|
w.WriteHeader(code)
|
||||||
|
err := textTmpl.ExpandVars(w, r, w)
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CommandRequireBasicAuth: {
|
CommandRequireBasicAuth: {
|
||||||
help: Help{
|
help: Help{
|
||||||
command: CommandRequireBasicAuth,
|
command: CommandRequireBasicAuth,
|
||||||
|
description: makeLines(
|
||||||
|
"Require HTTP basic authentication for incoming requests, e.g.:",
|
||||||
|
helpExample(CommandRequireBasicAuth, "Restricted Area"),
|
||||||
|
),
|
||||||
args: map[string]string{
|
args: map[string]string{
|
||||||
"realm": "the authentication realm",
|
"realm": "the authentication realm",
|
||||||
},
|
},
|
||||||
@@ -156,35 +224,63 @@ var commands = map[string]struct {
|
|||||||
},
|
},
|
||||||
build: func(args any) CommandHandler {
|
build: func(args any) CommandHandler {
|
||||||
realm := args.(string)
|
realm := args.(string)
|
||||||
return TerminatingCommand(func(w http.ResponseWriter, r *http.Request) {
|
return TerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
|
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CommandProxy: {
|
CommandProxy: {
|
||||||
help: Help{
|
help: Help{
|
||||||
command: CommandProxy,
|
command: CommandProxy,
|
||||||
|
description: makeLines(
|
||||||
|
"Proxy the request to the specified absolute URL, e.g.:",
|
||||||
|
helpExample(CommandProxy, "http://upstream:8080"),
|
||||||
|
),
|
||||||
args: map[string]string{
|
args: map[string]string{
|
||||||
"to": "the url to proxy to, must be an absolute URL",
|
"to": "the url to proxy to, must be an absolute URL",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
validate: validateAbsoluteURL,
|
validate: validateURL,
|
||||||
build: func(args any) CommandHandler {
|
build: func(args any) CommandHandler {
|
||||||
target := args.(*nettypes.URL)
|
target := args.(*nettypes.URL)
|
||||||
if target.Scheme == "" {
|
if target.Scheme == "" {
|
||||||
target.Scheme = "http"
|
target.Scheme = "http"
|
||||||
}
|
}
|
||||||
|
if target.Host == "" {
|
||||||
|
return TerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
url := target.URL
|
||||||
|
url.Host = routes.TryGetUpstreamHostPort(r)
|
||||||
|
if url.Host == "" {
|
||||||
|
return fmt.Errorf("no upstream host: %s", r.URL.String())
|
||||||
|
}
|
||||||
|
rp := reverseproxy.NewReverseProxy(url.Host, &url, gphttp.NewTransport())
|
||||||
|
r.URL.Path = target.Path
|
||||||
|
r.URL.RawPath = r.URL.EscapedPath()
|
||||||
|
r.RequestURI = r.URL.RequestURI()
|
||||||
|
rp.ServeHTTP(w, r)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
rp := reverseproxy.NewReverseProxy("", &target.URL, gphttp.NewTransport())
|
rp := reverseproxy.NewReverseProxy("", &target.URL, gphttp.NewTransport())
|
||||||
return TerminatingCommand(rp.ServeHTTP)
|
return TerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
rp.ServeHTTP(w, r)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
CommandSet: {
|
CommandSet: {
|
||||||
help: Help{
|
help: Help{
|
||||||
command: CommandSet,
|
command: CommandSet,
|
||||||
|
description: makeLines(
|
||||||
|
"Set a field in the request or response, e.g.:",
|
||||||
|
helpExample(CommandSet, "header", "User-Agent", "godoxy"),
|
||||||
|
),
|
||||||
args: map[string]string{
|
args: map[string]string{
|
||||||
"field": "the field to set",
|
"target": fmt.Sprintf("the target to set, can be %s", strings.Join(AllFields, ", ")),
|
||||||
"value": "the value to set",
|
"field": "the field to set",
|
||||||
|
"value": "the value to set",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
validate: func(args []string) (any, gperr.Error) {
|
validate: func(args []string) (any, gperr.Error) {
|
||||||
@@ -197,9 +293,14 @@ var commands = map[string]struct {
|
|||||||
CommandAdd: {
|
CommandAdd: {
|
||||||
help: Help{
|
help: Help{
|
||||||
command: CommandAdd,
|
command: CommandAdd,
|
||||||
|
description: makeLines(
|
||||||
|
"Add a value to a field in the request or response, e.g.:",
|
||||||
|
helpExample(CommandAdd, "header", "X-Foo", "bar"),
|
||||||
|
),
|
||||||
args: map[string]string{
|
args: map[string]string{
|
||||||
"field": "the field to add",
|
"target": fmt.Sprintf("the target to add, can be %s", strings.Join(AllFields, ", ")),
|
||||||
"value": "the value to add",
|
"field": "the field to add",
|
||||||
|
"value": "the value to add",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
validate: func(args []string) (any, gperr.Error) {
|
validate: func(args []string) (any, gperr.Error) {
|
||||||
@@ -212,8 +313,13 @@ var commands = map[string]struct {
|
|||||||
CommandRemove: {
|
CommandRemove: {
|
||||||
help: Help{
|
help: Help{
|
||||||
command: CommandRemove,
|
command: CommandRemove,
|
||||||
|
description: makeLines(
|
||||||
|
"Remove a field from the request or response, e.g.:",
|
||||||
|
helpExample(CommandRemove, "header", "User-Agent"),
|
||||||
|
),
|
||||||
args: map[string]string{
|
args: map[string]string{
|
||||||
"field": "the field to remove",
|
"target": fmt.Sprintf("the target to remove, can be %s", strings.Join(AllFields, ", ")),
|
||||||
|
"field": "the field to remove",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
validate: func(args []string) (any, gperr.Error) {
|
validate: func(args []string) (any, gperr.Error) {
|
||||||
@@ -223,17 +329,145 @@ var commands = map[string]struct {
|
|||||||
return args.(CommandHandler)
|
return args.(CommandHandler)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
CommandLog: {
|
||||||
|
isResponseHandler: true,
|
||||||
|
help: Help{
|
||||||
|
command: CommandLog,
|
||||||
|
description: makeLines(
|
||||||
|
"The template supports the following variables:",
|
||||||
|
helpListItem("Request", "the request object"),
|
||||||
|
helpListItem("Response", "the response object"),
|
||||||
|
"",
|
||||||
|
"Example:",
|
||||||
|
helpExample(CommandLog, "info", "/dev/stdout", "$req_method $req_url $status_code"),
|
||||||
|
),
|
||||||
|
args: map[string]string{
|
||||||
|
"level": "the log level",
|
||||||
|
"path": "the log path (/dev/stdout for stdout, /dev/stderr for stderr)",
|
||||||
|
"template": "the template to log",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: func(args []string) (any, gperr.Error) {
|
||||||
|
if len(args) != 3 {
|
||||||
|
return nil, ErrExpectThreeArgs
|
||||||
|
}
|
||||||
|
tmpl, err := validateTemplate(args[2], true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
level, err := validateLevel(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// NOTE: file will stay opened forever
|
||||||
|
// it leverages accesslog.NewFileIO so
|
||||||
|
// it will be opened only once for the same path
|
||||||
|
f, err := openFile(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &onLogArgs{level, f, tmpl}, nil
|
||||||
|
},
|
||||||
|
build: func(args any) CommandHandler {
|
||||||
|
level, f, tmpl := args.(*onLogArgs).Unpack()
|
||||||
|
var logger io.Writer
|
||||||
|
if f == stdout || f == stderr {
|
||||||
|
logger = logging.NewLoggerWithFixedLevel(level, f)
|
||||||
|
} else {
|
||||||
|
logger = f
|
||||||
|
}
|
||||||
|
return OnResponseCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
err := tmpl.ExpandVars(w, r, logger)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CommandNotify: {
|
||||||
|
isResponseHandler: true,
|
||||||
|
help: Help{
|
||||||
|
command: CommandNotify,
|
||||||
|
description: makeLines(
|
||||||
|
"The template supports the following variables:",
|
||||||
|
helpListItem("Request", "the request object"),
|
||||||
|
helpListItem("Response", "the response object"),
|
||||||
|
"",
|
||||||
|
"Example:",
|
||||||
|
helpExample(CommandNotify, "info", "ntfy", "Received request to $req_url", "$req_method $status_code"),
|
||||||
|
),
|
||||||
|
args: map[string]string{
|
||||||
|
"level": "the log level",
|
||||||
|
"provider": "the notification provider (must be defined in config `providers.notification`)",
|
||||||
|
"title": "the title of the notification",
|
||||||
|
"body": "the body of the notification",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: func(args []string) (any, gperr.Error) {
|
||||||
|
if len(args) != 4 {
|
||||||
|
return nil, ErrExpectFourArgs
|
||||||
|
}
|
||||||
|
titleTmpl, err := validateTemplate(args[2], false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bodyTmpl, err := validateTemplate(args[3], false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
level, err := validateLevel(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// TODO: validate provider
|
||||||
|
// currently it is not possible, because rule validation happens on UnmarshalYAMLValidate
|
||||||
|
// and we cannot call config.ActiveConfig.Load() because it will cause import cycle
|
||||||
|
|
||||||
|
// err = validateNotifProvider(args[1])
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
return &onNotifyArgs{level, args[1], titleTmpl, bodyTmpl}, nil
|
||||||
|
},
|
||||||
|
build: func(args any) CommandHandler {
|
||||||
|
level, provider, titleTmpl, bodyTmpl := args.(*onNotifyArgs).Unpack()
|
||||||
|
to := []string{provider}
|
||||||
|
|
||||||
|
return OnResponseCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
respBuf := bytes.NewBuffer(make([]byte, 0, titleTmpl.Len()+bodyTmpl.Len()))
|
||||||
|
|
||||||
|
err := titleTmpl.ExpandVars(w, r, respBuf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
titleLen := respBuf.Len()
|
||||||
|
err = bodyTmpl.ExpandVars(w, r, respBuf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b := respBuf.Bytes()
|
||||||
|
notif.Notify(¬if.LogMessage{
|
||||||
|
Level: level,
|
||||||
|
Title: string(b[:titleLen]),
|
||||||
|
Body: notif.MessageBodyBytes(b[titleLen:]),
|
||||||
|
To: to,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type onLogArgs = Tuple3[zerolog.Level, io.WriteCloser, templateString]
|
||||||
|
type onNotifyArgs = Tuple4[zerolog.Level, string, templateString, templateString]
|
||||||
|
|
||||||
// Parse implements strutils.Parser.
|
// Parse implements strutils.Parser.
|
||||||
func (cmd *Command) Parse(v string) error {
|
func (cmd *Command) Parse(v string) error {
|
||||||
lines := strutils.SplitLine(v)
|
executors := make([]CommandHandler, 0)
|
||||||
if len(lines) == 0 {
|
isResponseHandler := false
|
||||||
return nil
|
for line := range strings.SplitSeq(v, "\n") {
|
||||||
}
|
|
||||||
|
|
||||||
executors := make([]CommandHandler, 0, len(lines))
|
|
||||||
for _, line := range lines {
|
|
||||||
if line == "" {
|
if line == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -257,13 +491,21 @@ func (cmd *Command) Parse(v string) error {
|
|||||||
}
|
}
|
||||||
validArgs, err := builder.validate(args)
|
validArgs, err := builder.validate(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err.Subject(directive).Withf("%s", builder.help.String())
|
// Only attach help for the directive that failed, avoid bringing in unrelated KV errors
|
||||||
|
return err.Subject(directive).With(builder.help.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
executors = append(executors, builder.build(validArgs))
|
handler := builder.build(validArgs)
|
||||||
|
executors = append(executors, handler)
|
||||||
|
if builder.isResponseHandler || handler.IsResponseHandler() {
|
||||||
|
isResponseHandler = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(executors) == 0 {
|
if len(executors) == 0 {
|
||||||
|
cmd.raw = v
|
||||||
|
cmd.exec = nil
|
||||||
|
cmd.isResponseHandler = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,10 +516,14 @@ func (cmd *Command) Parse(v string) error {
|
|||||||
|
|
||||||
cmd.raw = v
|
cmd.raw = v
|
||||||
cmd.exec = exec
|
cmd.exec = exec
|
||||||
|
if exec.IsResponseHandler() {
|
||||||
|
isResponseHandler = true
|
||||||
|
}
|
||||||
|
cmd.isResponseHandler = isResponseHandler
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCmd(executors []CommandHandler) (CommandHandler, error) {
|
func buildCmd(executors []CommandHandler) (cmd CommandHandler, err error) {
|
||||||
for i, exec := range executors {
|
for i, exec := range executors {
|
||||||
switch exec.(type) {
|
switch exec.(type) {
|
||||||
case TerminatingCommand, BypassCommand:
|
case TerminatingCommand, BypassCommand:
|
||||||
@@ -308,6 +554,10 @@ func (cmd *Command) isBypass() bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
return cmd.exec.Handle(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
func (cmd *Command) String() string {
|
func (cmd *Command) String() string {
|
||||||
return cmd.raw
|
return cmd.raw
|
||||||
}
|
}
|
||||||
|
|||||||
385
internal/route/rules/do_log_test.go
Normal file
385
internal/route/rules/do_log_test.go
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockUpstream creates a simple upstream handler for testing
|
||||||
|
func mockUpstream(status int, body string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
w.Write([]byte(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockUpstreamWithHeaders creates an upstream that returns specific headers
|
||||||
|
func mockUpstreamWithHeaders(status int, body string, headers http.Header) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
maps.Copy(w.Header(), headers)
|
||||||
|
w.WriteHeader(status)
|
||||||
|
w.Write([]byte(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRules(data string, target *Rules) gperr.Error {
|
||||||
|
_, err := serialization.ConvertString(data, reflect.ValueOf(target))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogCommand_TemporaryFile(t *testing.T) {
|
||||||
|
upstream := mockUpstreamWithHeaders(200, "success response", http.Header{
|
||||||
|
"Content-Type": []string{"application/json"},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a temporary file for logging
|
||||||
|
tempFile, err := os.CreateTemp("", "test-log-*.log")
|
||||||
|
require.NoError(t, err)
|
||||||
|
tempFile.Close()
|
||||||
|
defer os.Remove(tempFile.Name())
|
||||||
|
|
||||||
|
var rules Rules
|
||||||
|
err = parseRules(fmt.Sprintf(`
|
||||||
|
- name: log-request-response
|
||||||
|
do: |
|
||||||
|
log info %q '$req_method $req_url $status_code $resp_header(Content-Type)'
|
||||||
|
`, tempFile.Name()), &rules)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
handler := rules.BuildHandler(upstream)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/api/users", nil)
|
||||||
|
req.Header.Set("User-Agent", "test-agent")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
assert.Equal(t, "success response", w.Body.String())
|
||||||
|
|
||||||
|
// Read and verify log content
|
||||||
|
content, err := os.ReadFile(tempFile.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
logContent := string(content)
|
||||||
|
|
||||||
|
assert.Equal(t, "POST /api/users 200 application/json\n", logContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogCommand_StdoutAndStderr(t *testing.T) {
|
||||||
|
upstream := mockUpstream(200, "success")
|
||||||
|
|
||||||
|
var rules Rules
|
||||||
|
err := parseRules(`
|
||||||
|
- name: log-stdout
|
||||||
|
do: |
|
||||||
|
log info /dev/stdout "stdout: $req_method $status_code"
|
||||||
|
- name: log-stderr
|
||||||
|
do: |
|
||||||
|
log error /dev/stderr "stderr: $req_path $status_code"
|
||||||
|
`, &rules)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
handler := rules.BuildHandler(upstream)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
// Note: We can't easily capture stdout/stderr in unit tests,
|
||||||
|
// but we can verify no errors occurred and the handler completed
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogCommand_DifferentLogLevels(t *testing.T) {
|
||||||
|
upstream := mockUpstream(404, "not found")
|
||||||
|
|
||||||
|
// Create temporary files for different log levels
|
||||||
|
infoFile, err := os.CreateTemp("", "test-info-*.log")
|
||||||
|
require.NoError(t, err)
|
||||||
|
infoFile.Close()
|
||||||
|
defer os.Remove(infoFile.Name())
|
||||||
|
|
||||||
|
warnFile, err := os.CreateTemp("", "test-warn-*.log")
|
||||||
|
require.NoError(t, err)
|
||||||
|
warnFile.Close()
|
||||||
|
defer os.Remove(warnFile.Name())
|
||||||
|
|
||||||
|
errorFile, err := os.CreateTemp("", "test-error-*.log")
|
||||||
|
require.NoError(t, err)
|
||||||
|
errorFile.Close()
|
||||||
|
defer os.Remove(errorFile.Name())
|
||||||
|
|
||||||
|
var rules Rules
|
||||||
|
err = parseRules(fmt.Sprintf(`
|
||||||
|
- name: log-info
|
||||||
|
do: |
|
||||||
|
log info %s "INFO: $req_method $status_code"
|
||||||
|
- name: log-warn
|
||||||
|
do: |
|
||||||
|
log warn %s "WARN: $req_path $status_code"
|
||||||
|
- name: log-error
|
||||||
|
do: |
|
||||||
|
log error %s "ERROR: $req_method $req_path $status_code"
|
||||||
|
`, infoFile.Name(), warnFile.Name(), errorFile.Name()), &rules)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
handler := rules.BuildHandler(upstream)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("DELETE", "/api/resource/123", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 404, w.Code)
|
||||||
|
|
||||||
|
// Verify each log file
|
||||||
|
infoContent, err := os.ReadFile(infoFile.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "INFO: DELETE 404", strings.TrimSpace(string(infoContent)))
|
||||||
|
|
||||||
|
warnContent, err := os.ReadFile(warnFile.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "WARN: /api/resource/123 404", strings.TrimSpace(string(warnContent)))
|
||||||
|
|
||||||
|
errorContent, err := os.ReadFile(errorFile.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "ERROR: DELETE /api/resource/123 404", strings.TrimSpace(string(errorContent)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogCommand_TemplateVariables(t *testing.T) {
|
||||||
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("X-Custom-Header", "custom-value")
|
||||||
|
w.Header().Set("Content-Length", "42")
|
||||||
|
w.WriteHeader(201)
|
||||||
|
w.Write([]byte("created"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create temporary file
|
||||||
|
tempFile, err := os.CreateTemp("", "test-template-*.log")
|
||||||
|
require.NoError(t, err)
|
||||||
|
tempFile.Close()
|
||||||
|
defer os.Remove(tempFile.Name())
|
||||||
|
|
||||||
|
var rules Rules
|
||||||
|
err = parseRules(fmt.Sprintf(`
|
||||||
|
- name: log-with-templates
|
||||||
|
do: |
|
||||||
|
log info %s 'Request: $req_method $req_url Host: $req_host User-Agent: $header(User-Agent) Response: $status_code Custom-Header: $resp_header(X-Custom-Header) Content-Length: $resp_header(Content-Length)'
|
||||||
|
`, tempFile.Name()), &rules)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
handler := rules.BuildHandler(upstream)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("PUT", "/api/resource", nil)
|
||||||
|
req.Header.Set("User-Agent", "test-client/1.0")
|
||||||
|
req.Host = "example.com"
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 201, w.Code)
|
||||||
|
|
||||||
|
// Verify log content
|
||||||
|
content, err := os.ReadFile(tempFile.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
logContent := strings.TrimSpace(string(content))
|
||||||
|
|
||||||
|
assert.Equal(t, "Request: PUT /api/resource Host: example.com User-Agent: test-client/1.0 Response: 201 Custom-Header: custom-value Content-Length: 42", logContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogCommand_ConditionalLogging(t *testing.T) {
|
||||||
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/error":
|
||||||
|
w.WriteHeader(500)
|
||||||
|
w.Write([]byte("internal server error"))
|
||||||
|
case "/notfound":
|
||||||
|
w.WriteHeader(404)
|
||||||
|
w.Write([]byte("not found"))
|
||||||
|
default:
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write([]byte("success"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create temporary files
|
||||||
|
successFile, err := os.CreateTemp("", "test-success-*.log")
|
||||||
|
require.NoError(t, err)
|
||||||
|
successFile.Close()
|
||||||
|
defer os.Remove(successFile.Name())
|
||||||
|
|
||||||
|
errorFile, err := os.CreateTemp("", "test-error-*.log")
|
||||||
|
require.NoError(t, err)
|
||||||
|
errorFile.Close()
|
||||||
|
defer os.Remove(errorFile.Name())
|
||||||
|
|
||||||
|
var rules Rules
|
||||||
|
err = parseRules(fmt.Sprintf(`
|
||||||
|
- name: log-success
|
||||||
|
on: status 2xx
|
||||||
|
do: |
|
||||||
|
log info %q "SUCCESS: $req_method $req_path $status_code"
|
||||||
|
- name: log-error
|
||||||
|
on: status 4xx | status 5xx
|
||||||
|
do: |
|
||||||
|
log error %q "ERROR: $req_method $req_path $status_code"
|
||||||
|
`, successFile.Name(), errorFile.Name()), &rules)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
handler := rules.BuildHandler(upstream)
|
||||||
|
|
||||||
|
// Test success request
|
||||||
|
req1 := httptest.NewRequest("GET", "/success", nil)
|
||||||
|
w1 := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w1, req1)
|
||||||
|
assert.Equal(t, 200, w1.Code)
|
||||||
|
|
||||||
|
// Test not found request
|
||||||
|
req2 := httptest.NewRequest("GET", "/notfound", nil)
|
||||||
|
w2 := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w2, req2)
|
||||||
|
assert.Equal(t, 404, w2.Code)
|
||||||
|
|
||||||
|
// Test server error request
|
||||||
|
req3 := httptest.NewRequest("POST", "/error", nil)
|
||||||
|
w3 := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w3, req3)
|
||||||
|
assert.Equal(t, 500, w3.Code)
|
||||||
|
|
||||||
|
// Verify success log
|
||||||
|
successContent, err := os.ReadFile(successFile.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
successLines := strings.Split(strings.TrimSpace(string(successContent)), "\n")
|
||||||
|
assert.Len(t, successLines, 1)
|
||||||
|
assert.Equal(t, "SUCCESS: GET /success 200", successLines[0])
|
||||||
|
|
||||||
|
// Verify error log
|
||||||
|
errorContent, err := os.ReadFile(errorFile.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
errorLines := strings.Split(strings.TrimSpace(string(errorContent)), "\n")
|
||||||
|
assert.Len(t, errorLines, 2)
|
||||||
|
assert.Equal(t, "ERROR: GET /notfound 404", errorLines[0])
|
||||||
|
assert.Equal(t, "ERROR: POST /error 500", errorLines[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogCommand_MultipleLogEntries(t *testing.T) {
|
||||||
|
upstream := mockUpstream(200, "response")
|
||||||
|
|
||||||
|
// Create temporary file
|
||||||
|
tempFile, err := os.CreateTemp("", "test-multiple-*.log")
|
||||||
|
require.NoError(t, err)
|
||||||
|
tempFile.Close()
|
||||||
|
defer os.Remove(tempFile.Name())
|
||||||
|
|
||||||
|
var rules Rules
|
||||||
|
err = parseRules(fmt.Sprintf(`
|
||||||
|
- name: log-multiple
|
||||||
|
do: |
|
||||||
|
log info %q "$req_method $req_path $status_code"`, tempFile.Name()), &rules)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
handler := rules.BuildHandler(upstream)
|
||||||
|
|
||||||
|
// Make multiple requests
|
||||||
|
requests := []struct {
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{"GET", "/users"},
|
||||||
|
{"POST", "/users"},
|
||||||
|
{"PUT", "/users/1"},
|
||||||
|
{"DELETE", "/users/1"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, reqInfo := range requests {
|
||||||
|
req := httptest.NewRequest(reqInfo.method, reqInfo.path, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all requests were logged
|
||||||
|
content, err := os.ReadFile(tempFile.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
logContent := strings.TrimSpace(string(content))
|
||||||
|
lines := strings.Split(logContent, "\n")
|
||||||
|
|
||||||
|
assert.Len(t, lines, len(requests))
|
||||||
|
|
||||||
|
for i, reqInfo := range requests {
|
||||||
|
expectedLog := reqInfo.method + " " + reqInfo.path + " 200"
|
||||||
|
assert.Equal(t, expectedLog, lines[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogCommand_FilePermissions(t *testing.T) {
|
||||||
|
upstream := mockUpstream(200, "success")
|
||||||
|
|
||||||
|
// Create a temporary directory
|
||||||
|
tempDir, err := os.MkdirTemp("", "test-log-dir")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create a log file path within the temp directory
|
||||||
|
logFilePath := filepath.Join(tempDir, "test.log")
|
||||||
|
|
||||||
|
var rules Rules
|
||||||
|
err = parseRules(fmt.Sprintf(`
|
||||||
|
- on: status 2xx
|
||||||
|
do: log info %q "$req_method $status_code"`, logFilePath), &rules)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
handler := rules.BuildHandler(upstream)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
|
||||||
|
// Verify file was created and is writable
|
||||||
|
_, err = os.Stat(logFilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test writing to the file again to ensure it's not closed
|
||||||
|
req2 := httptest.NewRequest("POST", "/test2", nil)
|
||||||
|
w2 := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w2, req2)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, w2.Code)
|
||||||
|
|
||||||
|
// Verify both entries are in the file
|
||||||
|
content, err := os.ReadFile(logFilePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
logContent := strings.TrimSpace(string(content))
|
||||||
|
lines := strings.Split(logContent, "\n")
|
||||||
|
|
||||||
|
assert.Len(t, lines, 2)
|
||||||
|
assert.Equal(t, "GET 200", lines[0])
|
||||||
|
assert.Equal(t, "POST 200", lines[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogCommand_InvalidTemplate(t *testing.T) {
|
||||||
|
var rules Rules
|
||||||
|
|
||||||
|
// Test with invalid template syntax
|
||||||
|
err := parseRules(`
|
||||||
|
- name: log-invalid
|
||||||
|
do: |
|
||||||
|
log info /dev/stdout "$invalid_var"`, &rules)
|
||||||
|
assert.ErrorIs(t, err, ErrUnexpectedVar)
|
||||||
|
}
|
||||||
326
internal/route/rules/do_set.go
Normal file
326
internal/route/rules/do_set.go
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
ioutils "github.com/yusing/goutils/io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
FieldHandler struct {
|
||||||
|
set, add, remove CommandHandler
|
||||||
|
}
|
||||||
|
FieldModifier string
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModFieldSet FieldModifier = "set"
|
||||||
|
ModFieldAdd FieldModifier = "add"
|
||||||
|
ModFieldRemove FieldModifier = "remove"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FieldHeader = "header"
|
||||||
|
FieldResponseHeader = "resp_header"
|
||||||
|
FieldQuery = "query"
|
||||||
|
FieldCookie = "cookie"
|
||||||
|
FieldBody = "body"
|
||||||
|
FieldResponseBody = "resp_body"
|
||||||
|
FieldStatusCode = "status"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AllFields = []string{FieldHeader, FieldResponseHeader, FieldQuery, FieldCookie, FieldBody, FieldResponseBody, FieldStatusCode}
|
||||||
|
|
||||||
|
// NOTE: should not use canonicalized header keys, respect to user's input
|
||||||
|
var modFields = map[string]struct {
|
||||||
|
help Help
|
||||||
|
validate ValidateFunc
|
||||||
|
builder func(args any) *FieldHandler
|
||||||
|
}{
|
||||||
|
FieldHeader: {
|
||||||
|
help: Help{
|
||||||
|
command: FieldHeader,
|
||||||
|
args: map[string]string{
|
||||||
|
"key": "the header key",
|
||||||
|
"value": "the header template",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: toKeyValueTemplate,
|
||||||
|
builder: func(args any) *FieldHandler {
|
||||||
|
k, tmpl := args.(*keyValueTemplate).Unpack()
|
||||||
|
return &FieldHandler{
|
||||||
|
set: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
v, err := tmpl.ExpandVarsToString(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Header[k] = []string{v}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
add: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
v, err := tmpl.ExpandVarsToString(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Header[k] = append(r.Header[k], v)
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
remove: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
delete(r.Header, k)
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FieldResponseHeader: {
|
||||||
|
help: Help{
|
||||||
|
command: FieldResponseHeader,
|
||||||
|
args: map[string]string{
|
||||||
|
"key": "the response header key",
|
||||||
|
"value": "the response header template",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: toKeyValueTemplate,
|
||||||
|
builder: func(args any) *FieldHandler {
|
||||||
|
k, tmpl := args.(*keyValueTemplate).Unpack()
|
||||||
|
return &FieldHandler{
|
||||||
|
set: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
v, err := tmpl.ExpandVarsToString(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.Header()[k] = []string{v}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
add: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
v, err := tmpl.ExpandVarsToString(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.Header()[k] = append(w.Header()[k], v)
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
remove: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
delete(w.Header(), k)
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FieldQuery: {
|
||||||
|
help: Help{
|
||||||
|
command: FieldQuery,
|
||||||
|
args: map[string]string{
|
||||||
|
"key": "the query key",
|
||||||
|
"value": "the query template",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: toKeyValueTemplate,
|
||||||
|
builder: func(args any) *FieldHandler {
|
||||||
|
k, tmpl := args.(*keyValueTemplate).Unpack()
|
||||||
|
return &FieldHandler{
|
||||||
|
set: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
v, err := tmpl.ExpandVarsToString(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
GetSharedData(w).UpdateQueries(r, func(queries url.Values) {
|
||||||
|
queries.Set(k, v)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
add: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
v, err := tmpl.ExpandVarsToString(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
GetSharedData(w).UpdateQueries(r, func(queries url.Values) {
|
||||||
|
queries.Add(k, v)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
remove: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
GetSharedData(w).UpdateQueries(r, func(queries url.Values) {
|
||||||
|
queries.Del(k)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FieldCookie: {
|
||||||
|
help: Help{
|
||||||
|
command: FieldCookie,
|
||||||
|
args: map[string]string{
|
||||||
|
"key": "the cookie key",
|
||||||
|
"value": "the cookie value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: toKeyValueTemplate,
|
||||||
|
builder: func(args any) *FieldHandler {
|
||||||
|
k, tmpl := args.(*keyValueTemplate).Unpack()
|
||||||
|
return &FieldHandler{
|
||||||
|
set: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
v, err := tmpl.ExpandVarsToString(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
GetSharedData(w).UpdateCookies(r, func(cookies []*http.Cookie) []*http.Cookie {
|
||||||
|
for i, c := range cookies {
|
||||||
|
if c.Name == k {
|
||||||
|
cookies[i].Value = v
|
||||||
|
return cookies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(cookies, &http.Cookie{Name: k, Value: v})
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
add: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
v, err := tmpl.ExpandVarsToString(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
GetSharedData(w).UpdateCookies(r, func(cookies []*http.Cookie) []*http.Cookie {
|
||||||
|
return append(cookies, &http.Cookie{Name: k, Value: v})
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
remove: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
GetSharedData(w).UpdateCookies(r, func(cookies []*http.Cookie) []*http.Cookie {
|
||||||
|
index := -1
|
||||||
|
for i, c := range cookies {
|
||||||
|
if c.Name == k {
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if index != -1 {
|
||||||
|
if len(cookies) == 1 {
|
||||||
|
return []*http.Cookie{}
|
||||||
|
}
|
||||||
|
return append(cookies[:index], cookies[index+1:]...)
|
||||||
|
}
|
||||||
|
return cookies
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FieldBody: {
|
||||||
|
help: Help{
|
||||||
|
command: FieldBody,
|
||||||
|
description: makeLines(
|
||||||
|
"Override the request body that will be sent to the upstream",
|
||||||
|
"The template supports the following variables:",
|
||||||
|
helpListItem("Request", "the request object"),
|
||||||
|
"",
|
||||||
|
"Example:",
|
||||||
|
helpExample(FieldBody, "HTTP STATUS: $req_method $req_path"),
|
||||||
|
),
|
||||||
|
args: map[string]string{
|
||||||
|
"template": "the body template",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: func(args []string) (any, gperr.Error) {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return nil, ErrExpectOneArg
|
||||||
|
}
|
||||||
|
return validateTemplate(args[0], true)
|
||||||
|
},
|
||||||
|
builder: func(args any) *FieldHandler {
|
||||||
|
tmpl := args.(templateString)
|
||||||
|
return &FieldHandler{
|
||||||
|
set: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
if r.Body != nil {
|
||||||
|
r.Body.Close()
|
||||||
|
r.Body = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bufPool := GetInitResponseModifier(w).BufPool()
|
||||||
|
b := bufPool.GetBuffer()
|
||||||
|
err := tmpl.ExpandVars(w, r, b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Body = ioutils.NewHookReadCloser(io.NopCloser(b), func() {
|
||||||
|
bufPool.PutBuffer(b)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FieldResponseBody: {
|
||||||
|
help: Help{
|
||||||
|
command: FieldResponseBody,
|
||||||
|
description: makeLines(
|
||||||
|
"Override the response body that will be sent to the client",
|
||||||
|
"The template supports the following variables:",
|
||||||
|
helpListItem("Request", "the request object"),
|
||||||
|
helpListItem("Response", "the response object"),
|
||||||
|
"",
|
||||||
|
"Example:",
|
||||||
|
helpExample(FieldResponseBody, "HTTP STATUS: $req_method $status_code"),
|
||||||
|
),
|
||||||
|
args: map[string]string{
|
||||||
|
"template": "the response body template",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: func(args []string) (any, gperr.Error) {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return nil, ErrExpectOneArg
|
||||||
|
}
|
||||||
|
return validateTemplate(args[0], true)
|
||||||
|
},
|
||||||
|
builder: func(args any) *FieldHandler {
|
||||||
|
tmpl := args.(templateString)
|
||||||
|
return &FieldHandler{
|
||||||
|
set: OnResponseCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
rm := GetInitResponseModifier(w)
|
||||||
|
rm.ResetBody()
|
||||||
|
return tmpl.ExpandVars(w, r, rm)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FieldStatusCode: {
|
||||||
|
help: Help{
|
||||||
|
command: FieldStatusCode,
|
||||||
|
description: makeLines(
|
||||||
|
"Override the status code that will be sent to the client, e.g.:",
|
||||||
|
helpExample(FieldStatusCode, "200"),
|
||||||
|
),
|
||||||
|
args: map[string]string{
|
||||||
|
"code": "the status code",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validate: func(args []string) (any, gperr.Error) {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return nil, ErrExpectOneArg
|
||||||
|
}
|
||||||
|
status, err := strconv.Atoi(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrInvalidArguments.With(err)
|
||||||
|
}
|
||||||
|
if status < 100 || status > 599 {
|
||||||
|
return nil, ErrInvalidArguments.Withf("status code must be between 100 and 599, got %d", status)
|
||||||
|
}
|
||||||
|
return status, nil
|
||||||
|
},
|
||||||
|
builder: func(args any) *FieldHandler {
|
||||||
|
status := args.(int)
|
||||||
|
return &FieldHandler{
|
||||||
|
set: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
GetInitResponseModifier(w).WriteHeader(status)
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
643
internal/route/rules/do_set_test.go
Normal file
643
internal/route/rules/do_set_test.go
Normal file
@@ -0,0 +1,643 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFieldHandler_Header(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
modifier FieldModifier
|
||||||
|
setup func(*http.Request)
|
||||||
|
verify func(*http.Request, *httptest.ResponseRecorder)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "set header",
|
||||||
|
key: "X-Test",
|
||||||
|
value: "test-value",
|
||||||
|
modifier: ModFieldSet,
|
||||||
|
setup: func(r *http.Request) {
|
||||||
|
r.Header.Set("X-Test", "old-value")
|
||||||
|
},
|
||||||
|
verify: func(r *http.Request, w *httptest.ResponseRecorder) {
|
||||||
|
got := r.Header.Get("X-Test")
|
||||||
|
assert.Equal(t, "test-value", got, "Expected header X-Test to be 'test-value'")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add header",
|
||||||
|
key: "X-Test",
|
||||||
|
value: "new-value",
|
||||||
|
modifier: ModFieldAdd,
|
||||||
|
setup: func(r *http.Request) {
|
||||||
|
r.Header.Set("X-Test", "existing-value")
|
||||||
|
},
|
||||||
|
verify: func(r *http.Request, w *httptest.ResponseRecorder) {
|
||||||
|
values := r.Header["X-Test"]
|
||||||
|
require.Len(t, values, 2, "Expected 2 header values")
|
||||||
|
assert.Equal(t, "existing-value", values[0], "Expected first value of X-Test header to be 'existing-value'")
|
||||||
|
assert.Equal(t, "new-value", values[1], "Expected second value of X-Test header to be 'new-value'")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove header",
|
||||||
|
key: "X-Test",
|
||||||
|
value: "",
|
||||||
|
modifier: ModFieldRemove,
|
||||||
|
setup: func(r *http.Request) {
|
||||||
|
r.Header.Set("X-Test", "to-be-removed")
|
||||||
|
},
|
||||||
|
verify: func(r *http.Request, w *httptest.ResponseRecorder) {
|
||||||
|
got := r.Header.Get("X-Test")
|
||||||
|
assert.Empty(t, got, "Expected header X-Test to be removed")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
tt.setup(req)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
tmpl, tErr := validateTemplate(tt.value, false)
|
||||||
|
if tErr != nil {
|
||||||
|
t.Fatalf("Failed to validate template: %v", tErr)
|
||||||
|
}
|
||||||
|
handler := modFields[FieldHeader].builder(&keyValueTemplate{tt.key, tmpl})
|
||||||
|
var cmd CommandHandler
|
||||||
|
switch tt.modifier {
|
||||||
|
case ModFieldSet:
|
||||||
|
cmd = handler.set
|
||||||
|
case ModFieldAdd:
|
||||||
|
cmd = handler.add
|
||||||
|
case ModFieldRemove:
|
||||||
|
cmd = handler.remove
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Handle(w, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Handler returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.verify(req, w)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldHandler_ResponseHeader(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
modifier FieldModifier
|
||||||
|
setup func(*httptest.ResponseRecorder)
|
||||||
|
verify func(*httptest.ResponseRecorder)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "set response header",
|
||||||
|
key: "X-Response-Test",
|
||||||
|
value: "response-value",
|
||||||
|
modifier: ModFieldSet,
|
||||||
|
verify: func(w *httptest.ResponseRecorder) {
|
||||||
|
got := w.Header().Get("X-Response-Test")
|
||||||
|
assert.Equal(t, "response-value", got, "Expected response header X-Response-Test to be 'response-value'")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add response header",
|
||||||
|
key: "X-Response-Test",
|
||||||
|
value: "additional-value",
|
||||||
|
modifier: ModFieldAdd,
|
||||||
|
setup: func(w *httptest.ResponseRecorder) {
|
||||||
|
w.Header().Set("X-Response-Test", "existing-value")
|
||||||
|
},
|
||||||
|
verify: func(w *httptest.ResponseRecorder) {
|
||||||
|
values := w.Header()["X-Response-Test"]
|
||||||
|
require.Len(t, values, 2)
|
||||||
|
assert.Equal(t, values[0], "existing-value")
|
||||||
|
assert.Equal(t, values[1], "additional-value")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove response header",
|
||||||
|
key: "X-Response-Test",
|
||||||
|
value: "",
|
||||||
|
modifier: ModFieldRemove,
|
||||||
|
verify: func(w *httptest.ResponseRecorder) {
|
||||||
|
assert.Empty(t, w.Header().Get("X-Response-Test"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
if tt.setup != nil {
|
||||||
|
tt.setup(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, tErr := validateTemplate(tt.value, false)
|
||||||
|
if tErr != nil {
|
||||||
|
t.Fatalf("Failed to validate template: %v", tErr)
|
||||||
|
}
|
||||||
|
handler := modFields[FieldResponseHeader].builder(&keyValueTemplate{tt.key, tmpl})
|
||||||
|
var cmd CommandHandler
|
||||||
|
switch tt.modifier {
|
||||||
|
case ModFieldSet:
|
||||||
|
cmd = handler.set
|
||||||
|
case ModFieldAdd:
|
||||||
|
cmd = handler.add
|
||||||
|
case ModFieldRemove:
|
||||||
|
cmd = handler.remove
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Handle(w, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Handler returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.verify(w)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldHandler_Query(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
modifier FieldModifier
|
||||||
|
setup func(*http.Request)
|
||||||
|
verify func(*http.Request)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "set query",
|
||||||
|
key: "test",
|
||||||
|
value: "new-value",
|
||||||
|
modifier: ModFieldSet,
|
||||||
|
setup: func(r *http.Request) {
|
||||||
|
r.URL.RawQuery = "test=old-value&other=keep"
|
||||||
|
},
|
||||||
|
verify: func(r *http.Request) {
|
||||||
|
got := r.URL.Query().Get("test")
|
||||||
|
assert.Equal(t, "new-value", got, "Expected query 'test' to be 'new-value'")
|
||||||
|
gotOther := r.URL.Query().Get("other")
|
||||||
|
assert.Equal(t, "keep", gotOther, "Expected query 'other' to be 'keep'")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add query",
|
||||||
|
key: "test",
|
||||||
|
value: "additional-value",
|
||||||
|
modifier: ModFieldAdd,
|
||||||
|
setup: func(r *http.Request) {
|
||||||
|
r.URL.RawQuery = "test=existing-value"
|
||||||
|
},
|
||||||
|
verify: func(r *http.Request) {
|
||||||
|
values := r.URL.Query()["test"]
|
||||||
|
require.Len(t, values, 2, "Expected 2 query values")
|
||||||
|
assert.Equal(t, "existing-value", values[0], "Expected first value of test query param to be 'existing-value'")
|
||||||
|
assert.Equal(t, "additional-value", values[1], "Expected second value of test query param to be 'additional-value'")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove query",
|
||||||
|
key: "test",
|
||||||
|
value: "",
|
||||||
|
modifier: ModFieldRemove,
|
||||||
|
setup: func(r *http.Request) {
|
||||||
|
r.URL.RawQuery = "test=to-be-removed&other=keep"
|
||||||
|
},
|
||||||
|
verify: func(r *http.Request) {
|
||||||
|
got := r.URL.Query().Get("test")
|
||||||
|
assert.Empty(t, got, "Expected query 'test' to be removed")
|
||||||
|
gotOther := r.URL.Query().Get("other")
|
||||||
|
assert.Equal(t, "keep", gotOther, "Expected query 'other' to be 'keep'")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
tt.setup(req)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
tmpl, tErr := validateTemplate(tt.value, false)
|
||||||
|
if tErr != nil {
|
||||||
|
t.Fatalf("Failed to validate template: %v", tErr)
|
||||||
|
}
|
||||||
|
handler := modFields[FieldQuery].builder(&keyValueTemplate{tt.key, tmpl})
|
||||||
|
var cmd CommandHandler
|
||||||
|
switch tt.modifier {
|
||||||
|
case ModFieldSet:
|
||||||
|
cmd = handler.set
|
||||||
|
case ModFieldAdd:
|
||||||
|
cmd = handler.add
|
||||||
|
case ModFieldRemove:
|
||||||
|
cmd = handler.remove
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Handle(w, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Handler returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.verify(req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldHandler_Cookie(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
modifier FieldModifier
|
||||||
|
setup func(*http.Request)
|
||||||
|
verify func(*http.Request)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "set cookie",
|
||||||
|
key: "test",
|
||||||
|
value: "new-value",
|
||||||
|
modifier: ModFieldSet,
|
||||||
|
setup: func(r *http.Request) {
|
||||||
|
r.AddCookie(&http.Cookie{Name: "test", Value: "old-value"})
|
||||||
|
},
|
||||||
|
verify: func(r *http.Request) {
|
||||||
|
cookie, err := r.Cookie("test")
|
||||||
|
assert.NoError(t, err, "Expected cookie 'test' to exist")
|
||||||
|
if err == nil {
|
||||||
|
assert.Equal(t, "new-value", cookie.Value, "Expected cookie 'test' to be 'new-value'")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add cookie",
|
||||||
|
key: "test",
|
||||||
|
value: "additional-value",
|
||||||
|
modifier: ModFieldAdd,
|
||||||
|
setup: func(r *http.Request) {
|
||||||
|
r.AddCookie(&http.Cookie{Name: "test", Value: "existing-value"})
|
||||||
|
},
|
||||||
|
verify: func(r *http.Request) {
|
||||||
|
cookies := r.Cookies()
|
||||||
|
testCookies := make([]string, 0)
|
||||||
|
for _, c := range cookies {
|
||||||
|
if c.Name == "test" {
|
||||||
|
testCookies = append(testCookies, c.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.Len(t, testCookies, 2, "Expected 2 cookies with name 'test'")
|
||||||
|
assert.Equal(t, "existing-value", testCookies[0], "Expected first value of 'test' cookie to be 'existing-value'")
|
||||||
|
assert.Equal(t, "additional-value", testCookies[1], "Expected second value of 'test' cookie to be 'additional-value'")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove cookie",
|
||||||
|
key: "test",
|
||||||
|
value: "",
|
||||||
|
modifier: ModFieldRemove,
|
||||||
|
setup: func(r *http.Request) {
|
||||||
|
r.AddCookie(&http.Cookie{Name: "test", Value: "to-be-removed"})
|
||||||
|
r.AddCookie(&http.Cookie{Name: "other", Value: "keep"})
|
||||||
|
},
|
||||||
|
verify: func(r *http.Request) {
|
||||||
|
_, err := r.Cookie("test")
|
||||||
|
assert.Error(t, err, "Expected cookie 'test' to be removed")
|
||||||
|
cookie, err := r.Cookie("other")
|
||||||
|
assert.NoError(t, err, "Expected cookie 'other' to exist")
|
||||||
|
if err == nil {
|
||||||
|
assert.Equal(t, "keep", cookie.Value, "Expected cookie 'other' to be 'keep'")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
tt.setup(req)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
tmpl, tErr := validateTemplate(tt.value, false)
|
||||||
|
if tErr != nil {
|
||||||
|
t.Fatalf("Failed to validate template: %v", tErr)
|
||||||
|
}
|
||||||
|
handler := modFields[FieldCookie].builder(&keyValueTemplate{tt.key, tmpl})
|
||||||
|
var cmd CommandHandler
|
||||||
|
switch tt.modifier {
|
||||||
|
case ModFieldSet:
|
||||||
|
cmd = handler.set
|
||||||
|
case ModFieldAdd:
|
||||||
|
cmd = handler.add
|
||||||
|
case ModFieldRemove:
|
||||||
|
cmd = handler.remove
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Handle(w, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Handler returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.verify(req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldHandler_Body(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
template string
|
||||||
|
setup func(*http.Request)
|
||||||
|
verify func(*http.Request)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "set body with template",
|
||||||
|
template: "Hello $req_method $req_path",
|
||||||
|
setup: func(r *http.Request) {
|
||||||
|
r.Method = "POST"
|
||||||
|
r.URL.Path = "/test"
|
||||||
|
},
|
||||||
|
verify: func(r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
assert.NoError(t, err, "Failed to read body")
|
||||||
|
expected := "Hello POST /test"
|
||||||
|
assert.Equal(t, expected, string(body), "Expected body content")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set body with existing body",
|
||||||
|
template: "Overridden",
|
||||||
|
setup: func(r *http.Request) {
|
||||||
|
r.Body = io.NopCloser(strings.NewReader("original body"))
|
||||||
|
},
|
||||||
|
verify: func(r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
assert.NoError(t, err, "Failed to read body")
|
||||||
|
assert.Equal(t, "Overridden", string(body), "Expected body to be 'Overridden'")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
tt.setup(req)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
tmpl, tErr := validateTemplate(tt.template, false)
|
||||||
|
if tErr != nil {
|
||||||
|
t.Fatalf("Failed to parse template: %v", tErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := modFields[FieldBody].builder(tmpl)
|
||||||
|
err := handler.set.Handle(w, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Handler returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.verify(req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldHandler_ResponseBody(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
template string
|
||||||
|
setup func(*http.Request)
|
||||||
|
verify func(*ResponseModifier)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "set response body with template",
|
||||||
|
template: "Response: $req_method $req_path",
|
||||||
|
setup: func(r *http.Request) {
|
||||||
|
r.Method = "GET"
|
||||||
|
r.URL.Path = "/api/test"
|
||||||
|
},
|
||||||
|
verify: func(rm *ResponseModifier) {
|
||||||
|
content := rm.buf.String()
|
||||||
|
expected := "Response: GET /api/test"
|
||||||
|
assert.Equal(t, expected, content, "Expected response body")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
tt.setup(req)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Create ResponseModifier wrapper
|
||||||
|
rm := NewResponseModifier(w)
|
||||||
|
|
||||||
|
tmpl, tErr := validateTemplate(tt.template, false)
|
||||||
|
if tErr != nil {
|
||||||
|
t.Fatalf("Failed to parse template: %v", tErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := modFields[FieldResponseBody].builder(tmpl)
|
||||||
|
err := handler.set.Handle(rm, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Handler returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.verify(rm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldHandler_StatusCode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status int
|
||||||
|
verify func(*httptest.ResponseRecorder)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "set status code 200",
|
||||||
|
status: 200,
|
||||||
|
verify: func(w *httptest.ResponseRecorder) {
|
||||||
|
assert.Equal(t, 200, w.Code, "Expected status code 200")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set status code 404",
|
||||||
|
status: 404,
|
||||||
|
verify: func(w *httptest.ResponseRecorder) {
|
||||||
|
assert.Equal(t, 404, w.Code, "Expected status code 404")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "set status code 500",
|
||||||
|
status: 500,
|
||||||
|
verify: func(w *httptest.ResponseRecorder) {
|
||||||
|
assert.Equal(t, 500, w.Code, "Expected status code 500")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
rm := NewResponseModifier(w)
|
||||||
|
var cmd Command
|
||||||
|
err := cmd.Parse(fmt.Sprintf("set %s %d", FieldStatusCode, tt.status))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Handler returned error: %v", err)
|
||||||
|
}
|
||||||
|
err = cmd.ServeHTTP(rm, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Handler returned error: %v", err)
|
||||||
|
}
|
||||||
|
rm.FlushRelease()
|
||||||
|
|
||||||
|
tt.verify(w)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldValidation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
field string
|
||||||
|
args []string
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "header valid",
|
||||||
|
field: FieldHeader,
|
||||||
|
args: []string{"key", "value"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "header invalid - missing value",
|
||||||
|
field: FieldHeader,
|
||||||
|
args: []string{"key"},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "response header valid",
|
||||||
|
field: FieldResponseHeader,
|
||||||
|
args: []string{"key", "value"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query valid",
|
||||||
|
field: FieldQuery,
|
||||||
|
args: []string{"key", "value"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cookie valid",
|
||||||
|
field: FieldCookie,
|
||||||
|
args: []string{"key", "value"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "body valid template",
|
||||||
|
field: FieldBody,
|
||||||
|
args: []string{"Hello $req_method"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "body invalid template syntax",
|
||||||
|
field: FieldBody,
|
||||||
|
args: []string{"Hello $invalid_field"},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "response body valid template",
|
||||||
|
field: FieldResponseBody,
|
||||||
|
args: []string{"Response: $req_method"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status code valid",
|
||||||
|
field: FieldStatusCode,
|
||||||
|
args: []string{"200"},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status code invalid - too low",
|
||||||
|
field: FieldStatusCode,
|
||||||
|
args: []string{"99"},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status code invalid - too high",
|
||||||
|
field: FieldStatusCode,
|
||||||
|
args: []string{"600"},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status code invalid - not a number",
|
||||||
|
field: FieldStatusCode,
|
||||||
|
args: []string{"not-a-number"},
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
field, exists := modFields[tt.field]
|
||||||
|
assert.True(t, exists, "Field %s does not exist", tt.field)
|
||||||
|
|
||||||
|
_, err := field.validate(tt.args)
|
||||||
|
if tt.wantError {
|
||||||
|
assert.Error(t, err, "Expected error but got none")
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err, "Expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllFields(t *testing.T) {
|
||||||
|
expectedFields := []string{
|
||||||
|
FieldHeader,
|
||||||
|
FieldResponseHeader,
|
||||||
|
FieldQuery,
|
||||||
|
FieldCookie,
|
||||||
|
FieldBody,
|
||||||
|
FieldResponseBody,
|
||||||
|
FieldStatusCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Len(t, AllFields, len(expectedFields), "Expected %d fields", len(expectedFields))
|
||||||
|
|
||||||
|
for _, expected := range expectedFields {
|
||||||
|
found := false
|
||||||
|
for _, actual := range AllFields {
|
||||||
|
if actual == expected {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "Expected field %s not found in AllFields", expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModFields(t *testing.T) {
|
||||||
|
for fieldName, field := range modFields {
|
||||||
|
// Test that each field has required components
|
||||||
|
assert.NotNil(t, field.validate, "Field %s has nil validate function", fieldName)
|
||||||
|
assert.NotNil(t, field.builder, "Field %s has nil builder function", fieldName)
|
||||||
|
assert.NotEmpty(t, field.help.command, "Field %s has empty help command", fieldName)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,10 +99,15 @@ func TestParseCommands(t *testing.T) {
|
|||||||
},
|
},
|
||||||
// proxy directive tests
|
// proxy directive tests
|
||||||
{
|
{
|
||||||
name: "proxy_valid",
|
name: "proxy_valid_abs",
|
||||||
input: "proxy http://localhost:8080",
|
input: "proxy http://localhost:8080",
|
||||||
wantErr: nil,
|
wantErr: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "proxy_valid_rel",
|
||||||
|
input: "proxy /foo/bar",
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "proxy_missing_target",
|
name: "proxy_missing_target",
|
||||||
input: "proxy",
|
input: "proxy",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user