Compare commits

...

28 Commits

Author SHA1 Message Date
yusing
bc19a54976 chore(deps): upgrade dependencies in submodules 2025-12-08 14:17:14 +08:00
yusing
12d999809f fix(http): correct Unwrap method and enhance error handling in Hijack method
- Updated the Hijack method in LazyResponseModifier and ResponseModifier to return a wrapped error for unsupported hijacking.
- Added a nil check in LazyResponseModifier's Unwrap method to ensure safe access to the underlying ResponseWriter.
2025-12-08 14:06:58 +08:00
yusing
6771293336 fix(middleware): enhance response modification handling in ServeHTTP
- Replaced ResponseModifier with new LazyResponseModifier.
- Added logic to skip modification for non-HTML content.
2025-12-08 13:45:53 +08:00
yusing
d240c9dfee fix(io): limit buffer size to 16KB to avoid high memory usage and improve context propagation 2025-12-08 10:46:00 +08:00
yusing
c7eda38933 refactor(route): simplify context handling in RouteContext
- Removed unnecessary requestInternal struct and directly accessed the context field of http.Request.
- Simplified the initialization of ctxFieldOffset.
2025-12-05 18:26:34 +08:00
yusing
09caa888ad refactor(config): update config structures to use strutils.Redacted for sensitive fields
- Modified Config structs in various packages to replace string fields with strutils.Redacted to prevent logging sensitive information.
- Updated serialization methods to accommodate new data types.
- Adjusted API token handling in Proxmox configuration.
2025-12-05 18:26:16 +08:00
yusing
e41a487371 chore: remove go.work 2025-12-05 17:51:22 +08:00
yusing
7c08a8da2e Revert "ci: Add workflow to automatically merge main into compat on tag push"
This reverts commit 9930f3fa2e.
2025-12-05 16:29:45 +08:00
yusing
82df824490 chore: go mod tidy 2025-12-05 16:20:29 +08:00
yusing
2f341001c1 chore(Makefile): add socket-proxy to docker build test and update build command syntax 2025-12-05 16:10:14 +08:00
yusing
25ee8041da refactor(http,rules): move SharedData and ResponseModifier to httputils
- implemented dependency injection for rule auth handler
2025-12-05 16:06:36 +08:00
yusing
8687a57b6c fix(Dockerfile): exclude goutils in mod caching stage 2025-12-05 01:29:30 +08:00
yusing
3f4ed31e46 fix(middleware): skip modify response for websocket and event-stream requests in ServeHTTP 2025-12-05 01:18:27 +08:00
yusing
9930f3fa2e ci: Add workflow to automatically merge main into compat on tag push 2025-12-05 01:11:50 +08:00
yusing
2157545e17 fix(route): nil panic when used as an idlewatcher dependency 2025-12-05 01:10:48 +08:00
yusing
f721395ff0 refactor(healthcheck): agent health check 2025-12-05 00:45:24 +08:00
yusing
0dc7c59af1 refactor(deps): upgrade go to 1.25.5; isolate dependencies for reverseproxy, websocket and server modules 2025-12-05 00:36:16 +08:00
yusing
e3fe126a5c chore(example): introduce health check configuration defaults in example config 2025-12-04 18:08:26 +08:00
yusing
aa2575696d fix(http): handle 0 content length properly in some cases 2025-12-04 17:33:01 +08:00
yusing
c1f9c2c957 fix(middleware): skip modification for HEAD requests in ModifyHTML middleware 2025-12-04 17:27:26 +08:00
yusing
c098fef615 fix(http): enhance Content-Length handling in ResponseModifier
- Introduced origContentLength and bodyModified fields to track original content length and body modification status.
- Updated ContentLength and ContentLengthStr methods to return accurate content length based on body modification state.
- Adjusted Write and FlushRelease methods to ensure proper handling of Content-Length header.
- Modified middleware to use the new ContentLengthStr method.
2025-12-04 17:26:15 +08:00
yusing
9cdc985fb0 fix(tests): correct test expectations for middleware bypass and rules 2025-12-04 16:18:14 +08:00
yusing
2034738422 refactor(labels): refine wildcard expansion logic and tests
- Added multiple test cases for the ExpandWildcard function to cover various scenarios including basic wildcards, no wildcards, empty labels, and YAML configurations.
- Improved handling of nested maps and invalid YAML inputs.
- Ensured that explicit labels and reference aliases are correctly processed and expanded.
2025-12-04 16:16:43 +08:00
yusing
55a42b81de refactor(healthcheck): streamline health check configuration and defaults
- Moved health check constants from common package alongside type definition.
- Updated health check configuration to use struct directly instead of pointers.
- Introduced global default health check config
2025-12-04 15:19:10 +08:00
yusing
48627753d6 refactor(routes): simplify route exclusion check and health check defaults 2025-12-04 15:12:35 +08:00
yusing
09b514393d chore(deps): upgrade go version and dependencies 2025-12-04 12:18:12 +08:00
yusing
3b2ae5dbd6 refactor: move some utility functions to goutils and update references 2025-12-04 12:17:33 +08:00
yusing
fac3d67a51 chore; update goutils 2025-11-23 11:46:04 +08:00
84 changed files with 853 additions and 1264 deletions

View File

@@ -1,5 +1,5 @@
# Stage 1: deps
FROM golang:1.25.4-alpine AS deps
FROM golang:1.25.5-alpine AS deps
HEALTHCHECK NONE
# package version does not matter
@@ -19,7 +19,9 @@ COPY go.mod go.sum ./
# remove godoxy stuff from go.mod first
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/go/pkg/mod \
sed -i '/^module github\.com\/yusing\/godoxy/!{/github\.com\/yusing\/godoxy/d}' go.mod && go mod download -x
sed -i '/^module github\.com\/yusing\/godoxy/!{/github\.com\/yusing\/godoxy/d}' go.mod && \
sed -i '/^module github\.com\/yusing\/goutils/!{/github\.com\/yusing\/goutils/d}' go.mod && \
go mod download -x
# Stage 2: builder
FROM deps AS builder

View File

@@ -80,6 +80,7 @@ test:
docker-build-test:
docker build -t godoxy .
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
docker build --build-arg=MAKE_ARGS=socket-proxy=1 -t godoxy-socket-proxy .
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
@@ -110,7 +111,7 @@ mod-tidy:
build:
mkdir -p $(shell dirname ${BIN_PATH})
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
${POST_BUILD}
run:

View File

@@ -1,14 +1,16 @@
module github.com/yusing/godoxy/agent
go 1.25.4
go 1.25.5
replace github.com/yusing/godoxy => ..
replace github.com/yusing/godoxy/socketproxy => ../socket-proxy
replace github.com/shirou/gopsutil/v4 => ../internal/gopsutil
replace github.com/yusing/goutils => ../goutils
replace (
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
github.com/yusing/godoxy => ../
github.com/yusing/godoxy/socketproxy => ../socket-proxy
github.com/yusing/goutils => ../goutils
github.com/yusing/goutils/http/reverseproxy => ../goutils/http/reverseproxy
github.com/yusing/goutils/http/websocket => ../goutils/http/websocket
github.com/yusing/goutils/server => ../goutils/server
)
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
@@ -20,9 +22,11 @@ require (
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1
github.com/valyala/fasthttp v1.68.0
github.com/yusing/godoxy v0.20.2
github.com/yusing/godoxy v0.20.10
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
github.com/yusing/goutils v0.7.0
github.com/yusing/goutils/http/reverseproxy v0.0.0-00010101000000-000000000000
github.com/yusing/goutils/server v0.0.0-20250927113415-dd977d2edaeb
)
require (
@@ -30,6 +34,7 @@ require (
github.com/PuerkitoBio/goquery v1.11.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
@@ -37,50 +42,61 @@ require (
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/diskfs/go-diskfs v1.7.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.0.1+incompatible // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/docker/cli v29.1.2+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-acme/lego/v4 v4.29.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/goccy/go-yaml v1.19.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gotify/server/v2 v2.7.3 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/luthermonson/go-proxmox v0.2.3 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.52.0 // indirect
github.com/moby/moby/client v0.1.0 // indirect
github.com/moby/moby/client v0.2.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.56.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/samber/slog-common v0.19.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.10 // indirect
github.com/shirou/gopsutil/v4 v4.25.11 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
@@ -89,18 +105,22 @@ require (
github.com/vincent-petithory/dataurl v1.0.0 // indirect
github.com/yusing/ds v0.3.1 // indirect
github.com/yusing/gointernals v0.1.16 // indirect
github.com/yusing/goutils/http/websocket v0.0.0-00010101000000-000000000000 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // 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/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -2,6 +2,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/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=
github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
github.com/andybalholm/brotli v1.2.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=
@@ -22,8 +24,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -35,14 +37,16 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.0.1+incompatible h1:EnvMEAR9Ro5xQEKbMitlabj5vCDY0vwcDyY/Lsow7FQ=
github.com/docker/cli v29.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.1.2+incompatible h1:s4QI7drXpIo78OM+CwuthPsO5kCf8cpNsck5PsLVTH8=
github.com/docker/cli v29.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -53,8 +57,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-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-acme/lego/v4 v4.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
github.com/go-acme/lego/v4 v4.28.1/go.mod h1:bzjilr03IgbaOwlH396hq5W56Bi0/uoRwW/JM8hP7m4=
github.com/go-acme/lego/v4 v4.29.0 h1:vKMEtvoKb0gOO9rWO9zMBwE4CgI5A5CWDsK4QEeBqzo=
github.com/go-acme/lego/v4 v4.29.0/go.mod h1:rnYyDj1NdDd9y1dHkVuUS97j7bfe9I61+oY9odKaHM8=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -73,12 +77,14 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
@@ -94,12 +100,16 @@ 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/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/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -129,8 +139,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8=
github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -144,9 +154,13 @@ github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -154,10 +168,10 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
@@ -192,6 +206,8 @@ 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/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
@@ -223,8 +239,8 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -233,8 +249,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -268,8 +284,10 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@@ -1,6 +1,7 @@
package handler
import (
"context"
"fmt"
"net/http"
"net/url"
@@ -12,8 +13,6 @@ import (
"github.com/yusing/godoxy/internal/watcher/health/monitor"
)
var defaultHealthConfig = types.DefaultHealthConfig()
func CheckHealth(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
scheme := query.Get("scheme")
@@ -49,7 +48,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
Scheme: scheme,
Host: host,
Path: path,
}, defaultHealthConfig).CheckHealth()
}, healthCheckConfigFromRequest(r)).CheckHealth()
case "tcp", "udp":
host := query.Get("host")
if host == "" {
@@ -68,7 +67,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
result, err = monitor.NewRawHealthMonitor(&url.URL{
Scheme: scheme,
Host: host,
}, defaultHealthConfig).CheckHealth()
}, healthCheckConfigFromRequest(r)).CheckHealth()
}
if err != nil {
@@ -80,3 +79,13 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
sonic.ConfigDefault.NewEncoder(w).Encode(result)
}
func healthCheckConfigFromRequest(r *http.Request) types.HealthCheckConfig {
// we only need timeout and base context because it's one shot request
return types.HealthCheckConfig{
Timeout: types.HealthCheckTimeoutDefault,
BaseContext: func() context.Context {
return r.Context()
},
}
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/yusing/godoxy/internal/metrics/systeminfo"
"github.com/yusing/godoxy/internal/metrics/uptime"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/route/rules"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/task"
@@ -58,9 +59,12 @@ func main() {
}
config.StartProxyServers()
if err := auth.Initialize(); err != nil {
log.Fatal().Err(err).Msg("failed to initialize authentication")
}
rules.InitAuthHandler(auth.AuthOrProceed)
// API Handler needs to start after auth is initialized.
server.StartServer(task.RootTask("api_server", false), server.Options{
Name: "api",

View File

@@ -88,6 +88,12 @@ entrypoint:
# - name: default
# do: proxy http://other-proxy:8080
defaults:
healthcheck:
interval: 5s
timeout: 15s
retries: 3
providers:
# include files are standalone yaml files under `config/` directory
#

62
go.mod
View File

@@ -1,23 +1,24 @@
module github.com/yusing/godoxy
go 1.25.4
go 1.25.5
replace github.com/yusing/godoxy/agent => ./agent
replace github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
replace github.com/coreos/go-oidc/v3 => ./internal/go-oidc
replace github.com/shirou/gopsutil/v4 => ./internal/gopsutil
replace github.com/yusing/goutils => ./goutils
replace (
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
github.com/shirou/gopsutil/v4 => ./internal/gopsutil
github.com/yusing/godoxy/agent => ./agent
github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
github.com/yusing/goutils => ./goutils
github.com/yusing/goutils/http/reverseproxy => ./goutils/http/reverseproxy
github.com/yusing/goutils/http/websocket => ./goutils/http/websocket
github.com/yusing/goutils/server => ./goutils/server
)
require (
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon
github.com/coreos/go-oidc/v3 v3.16.0 // oidc authentication
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
github.com/fsnotify/fsnotify v1.9.0 // file watcher
github.com/gin-gonic/gin v1.11.0 // api server
github.com/go-acme/lego/v4 v4.28.1 // acme client
github.com/go-acme/lego/v4 v4.29.0 // acme client
github.com/go-playground/validator/v10 v10.28.0 // validator
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
@@ -27,7 +28,7 @@ require (
github.com/puzpuzpuz/xsync/v4 v4.2.0 // lock free map for concurrent operations
github.com/rs/zerolog v1.34.0 // logging
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
golang.org/x/crypto v0.44.0 // encrypting password with bcrypt
golang.org/x/crypto v0.45.0 // encrypting password with bcrypt
golang.org/x/net v0.47.0 // HTTP header utilities
golang.org/x/oauth2 v0.33.0 // oauth2 authentication
golang.org/x/sync v0.18.0
@@ -37,23 +38,26 @@ require (
require (
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
github.com/bytedance/sonic v1.14.2 // fast json parsing
github.com/docker/cli v29.0.1+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
github.com/docker/cli v29.1.2+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
github.com/goccy/go-yaml v1.19.0 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
github.com/luthermonson/go-proxmox v0.2.3 // proxmox API client
github.com/moby/moby/api v1.52.0 // docker API
github.com/moby/moby/client v0.1.0 // docker client
github.com/moby/moby/client v0.2.1 // docker client
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
github.com/quic-go/quic-go v0.56.0 // http3 support
github.com/shirou/gopsutil/v4 v4.25.10 // system information
github.com/quic-go/quic-go v0.57.1 // http3 support
github.com/shirou/gopsutil/v4 v4.25.11 // system information
github.com/spf13/afero v1.15.0 // afero for file system operations
github.com/stretchr/testify v1.11.1 // testing framework
github.com/valyala/fasthttp v1.68.0 // fast http for health check
github.com/yusing/ds v0.3.1 // data structures and algorithms
github.com/yusing/godoxy/agent v0.0.0-20251114142829-a291a49a0e42
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251114142829-a291a49a0e42
github.com/yusing/godoxy/agent v0.0.0-20251204100826-e3fe126a5c12
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251204100826-e3fe126a5c12
github.com/yusing/gointernals v0.1.16
github.com/yusing/goutils v0.7.0
github.com/yusing/goutils/http/reverseproxy v0.0.0-00010101000000-000000000000
github.com/yusing/goutils/http/websocket v0.0.0-00010101000000-000000000000
github.com/yusing/goutils/server v0.0.0-20250927113415-dd977d2edaeb
)
require (
@@ -112,7 +116,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/samber/slog-common v0.19.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
@@ -131,9 +135,9 @@ require (
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/api v0.257.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@@ -152,15 +156,15 @@ require (
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-resty/resty/v2 v2.16.5 // indirect
github.com/go-resty/resty/v2 v2.17.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/linode/linodego v1.61.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/stretchr/objx v0.5.3 // indirect
@@ -170,7 +174,7 @@ require (
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vultr/govultr/v3 v3.24.0 // indirect
github.com/vultr/govultr/v3 v3.25.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/arch v0.23.0 // indirect
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba // indirect

64
go.sum
View File

@@ -71,8 +71,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.0.1+incompatible h1:EnvMEAR9Ro5xQEKbMitlabj5vCDY0vwcDyY/Lsow7FQ=
github.com/docker/cli v29.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.1.2+incompatible h1:s4QI7drXpIo78OM+CwuthPsO5kCf8cpNsck5PsLVTH8=
github.com/docker/cli v29.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -93,8 +93,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-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-acme/lego/v4 v4.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
github.com/go-acme/lego/v4 v4.28.1/go.mod h1:bzjilr03IgbaOwlH396hq5W56Bi0/uoRwW/JM8hP7m4=
github.com/go-acme/lego/v4 v4.29.0 h1:vKMEtvoKb0gOO9rWO9zMBwE4CgI5A5CWDsK4QEeBqzo=
github.com/go-acme/lego/v4 v4.29.0/go.mod h1:rnYyDj1NdDd9y1dHkVuUS97j7bfe9I61+oY9odKaHM8=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -115,16 +115,16 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-resty/resty/v2 v2.17.0 h1:pW9DeXcaL4Rrym4EZ8v7L19zZiIlWPg5YXAcVmt+gN0=
github.com/go-resty/resty/v2 v2.17.0/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
@@ -169,8 +169,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/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -208,8 +208,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8=
github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -217,10 +217,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 h1:kR8dHXC53heV4fttilXXkDkGmkdC5bvQ2XgbVBoD+ns=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 h1:559XLHTF3F1A0J03PCIk6LlR0G9CmhHEDCm/TSk5BWQ=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1/go.mod h1:1FfSn6xcdK+wrNxUhtAAhLjYrwk9z8X3p3CNHwTc3zQ=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0 h1:bppmFqrJ87U4gWilemAW9oa4Qepf2JBTK/mPgaZLP2A=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0 h1:IHPZs4Mo/lxyo+gYB+baheb2kGmHtNGQk2DKPDHqPjA=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0/go.mod h1:yELd0uJLiIyv9sGIh5ZRCHEB1B2QFNURWkQIMqb3ZwE=
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -249,10 +249,10 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
@@ -303,8 +303,8 @@ github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFn
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/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/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/vultr/govultr/v3 v3.25.0 h1:rS8/Vdy8HlHArwmD4MtLY+hbbpYAbcnZueZrE6b0oUg=
github.com/vultr/govultr/v3 v3.25.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=
@@ -334,8 +334,8 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
@@ -346,8 +346,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -432,16 +432,16 @@ 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=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY=
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/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/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

Submodule goutils updated: ca5989aeda...3fd00b70fa

View File

@@ -14,7 +14,6 @@ import (
"github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/maxmind"
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
@@ -82,7 +81,7 @@ var ActiveConfig atomic.Pointer[Config]
const cacheTTL = 1 * time.Minute
func (c *checkCache) Expired() bool {
return c.created.Add(cacheTTL).Before(utils.TimeNow())
return c.created.Add(cacheTTL).Before(time.Now())
}
// TODO: add stats
@@ -180,7 +179,7 @@ func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
c.ipCache.Store(info.Str, &checkCache{
IPInfo: info,
allow: allow,
created: utils.TimeNow(),
created: time.Now(),
})
}

View File

@@ -6,8 +6,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/fs"
)
type ListFilesResponse struct {
@@ -35,7 +35,7 @@ func List(c *gin.Context) {
}
// config/
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
files, err := fs.ListFiles(common.ConfigBasePath, 0, true)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to list files"))
return
@@ -48,7 +48,7 @@ func List(c *gin.Context) {
}
// config/middlewares/
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
mids, err := fs.ListFiles(common.MiddlewareComposeBasePath, 0, true)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to list files"))
return

View File

@@ -12,6 +12,7 @@ import (
"github.com/yusing/godoxy/internal/route/rules"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http"
)
type RawRule struct {
@@ -348,7 +349,7 @@ func checkMatchedRules(rulesList rules.Rules, w http.ResponseWriter, r *http.Req
var matched []string
// Create a ResponseModifier to properly check rules
rm := rules.NewResponseModifier(w)
rm := httputils.NewResponseModifier(w)
for _, rule := range rulesList {
// Check if rule matches

View File

@@ -15,18 +15,18 @@ import (
"github.com/go-acme/lego/v4/lego"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
type Config struct {
Email string `json:"email,omitempty"`
Domains []string `json:"domains,omitempty"`
CertPath string `json:"cert_path,omitempty"`
KeyPath string `json:"key_path,omitempty"`
ACMEKeyPath string `json:"acme_key_path,omitempty"`
Provider string `json:"provider,omitempty"`
Options map[string]any `json:"options,omitempty"`
Email string `json:"email,omitempty"`
Domains []string `json:"domains,omitempty"`
CertPath string `json:"cert_path,omitempty"`
KeyPath string `json:"key_path,omitempty"`
ACMEKeyPath string `json:"acme_key_path,omitempty"`
Provider string `json:"provider,omitempty"`
Options map[string]strutils.Redacted `json:"options,omitempty"`
Resolvers []string `json:"resolvers,omitempty"`
@@ -96,7 +96,7 @@ func (cfg *Config) Validate() gperr.Error {
if cfg.Provider != ProviderCustom {
b.Add(ErrUnknownProvider.
Subject(cfg.Provider).
With(gperr.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
With(gperr.DoYouMeanField(cfg.Provider, Providers)))
}
} else {
provider, err := providerConstructor(cfg.Options)

View File

@@ -4,9 +4,10 @@ import (
"github.com/go-acme/lego/v4/challenge"
"github.com/yusing/godoxy/internal/serialization"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
type Generator func(map[string]any) (challenge.Provider, gperr.Error)
type Generator func(map[string]strutils.Redacted) (challenge.Provider, gperr.Error)
var Providers = make(map[string]Generator)
@@ -14,10 +15,10 @@ func DNSProvider[CT any, PT challenge.Provider](
defaultCfg func() *CT,
newProvider func(*CT) (PT, error),
) Generator {
return func(opt map[string]any) (challenge.Provider, gperr.Error) {
return func(opt map[string]strutils.Redacted) (challenge.Provider, gperr.Error) {
cfg := defaultCfg()
if len(opt) > 0 {
err := serialization.MapUnmarshalValidate(opt, &cfg)
err := serialization.MapUnmarshalValidate(serialization.ToSerializedObject(opt), &cfg)
if err != nil {
return nil, err
}

View File

@@ -1,9 +1,5 @@
package common
import (
"time"
)
// file, folder structure
const (
@@ -38,10 +34,6 @@ var RequiredDirectories = []string{
const DockerHostFromEnv = "$DOCKER_HOST"
const (
HealthCheckIntervalDefault = 5 * time.Second
HealthCheckTimeoutDefault = 5 * time.Second
HealthCheckDownNotifyDelayDefault = 15 * time.Second
WakeTimeoutDefault = "3m"
StopTimeoutDefault = "3m"
StopMethodDefault = "stop"

View File

@@ -14,6 +14,7 @@ import (
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/proxmox"
"github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
)
@@ -25,8 +26,12 @@ type (
Providers Providers `json:"providers"`
MatchDomains []string `json:"match_domains" validate:"domain_name"`
Homepage homepage.Config `json:"homepage"`
Defaults Defaults `json:"defaults"`
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
}
Defaults struct {
HealthCheck types.HealthCheckConfig `json:"healthcheck"`
}
Providers struct {
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"`

View File

@@ -1,12 +1,12 @@
module github.com/yusing/godoxy/internal/dnsproviders
go 1.25.4
go 1.25.5
replace github.com/yusing/godoxy => ../..
require (
github.com/go-acme/lego/v4 v4.28.1
github.com/yusing/godoxy v0.20.8
github.com/go-acme/lego/v4 v4.29.0
github.com/yusing/godoxy v0.20.11
)
require (
@@ -37,8 +37,8 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/go-resty/resty/v2 v2.16.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/go-resty/resty/v2 v2.17.0 // indirect
github.com/goccy/go-yaml v1.19.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
@@ -59,8 +59,8 @@ require (
github.com/miekg/dns v1.1.68 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
@@ -72,7 +72,7 @@ require (
github.com/stretchr/objx v0.5.3 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/vultr/govultr/v3 v3.24.0 // indirect
github.com/vultr/govultr/v3 v3.25.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusing/gointernals v0.1.16 // indirect
github.com/yusing/goutils v0.7.0 // indirect
@@ -81,10 +81,9 @@ require (
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
@@ -92,9 +91,9 @@ require (
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/api v0.257.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

View File

@@ -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/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-acme/lego/v4 v4.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
github.com/go-acme/lego/v4 v4.28.1/go.mod h1:bzjilr03IgbaOwlH396hq5W56Bi0/uoRwW/JM8hP7m4=
github.com/go-acme/lego/v4 v4.29.0 h1:vKMEtvoKb0gOO9rWO9zMBwE4CgI5A5CWDsK4QEeBqzo=
github.com/go-acme/lego/v4 v4.29.0/go.mod h1:rnYyDj1NdDd9y1dHkVuUS97j7bfe9I61+oY9odKaHM8=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -74,10 +74,10 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/go-resty/resty/v2 v2.17.0 h1:pW9DeXcaL4Rrym4EZ8v7L19zZiIlWPg5YXAcVmt+gN0=
github.com/go-resty/resty/v2 v2.17.0/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
@@ -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/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 h1:kR8dHXC53heV4fttilXXkDkGmkdC5bvQ2XgbVBoD+ns=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 h1:559XLHTF3F1A0J03PCIk6LlR0G9CmhHEDCm/TSk5BWQ=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1/go.mod h1:1FfSn6xcdK+wrNxUhtAAhLjYrwk9z8X3p3CNHwTc3zQ=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0 h1:bppmFqrJ87U4gWilemAW9oa4Qepf2JBTK/mPgaZLP2A=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.0/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0 h1:IHPZs4Mo/lxyo+gYB+baheb2kGmHtNGQk2DKPDHqPjA=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.0/go.mod h1:yELd0uJLiIyv9sGIh5ZRCHEB1B2QFNURWkQIMqb3ZwE=
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/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
@@ -178,8 +178,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/vultr/govultr/v3 v3.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.25.0 h1:rS8/Vdy8HlHArwmD4MtLY+hbbpYAbcnZueZrE6b0oUg=
github.com/vultr/govultr/v3 v3.25.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
@@ -208,8 +208,8 @@ go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
@@ -233,16 +233,16 @@ golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ
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/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY=
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/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/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -17,7 +17,6 @@ import (
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
)
@@ -224,7 +223,7 @@ func setPrivateHostname(c *types.Container, helper containerHelper) {
}
}
}
nearest := gperr.DoYouMean(utils.NearestField(c.Network, helper.NetworkSettings.Networks))
nearest := gperr.DoYouMeanField(c.Network, helper.NetworkSettings.Networks)
addError(c, fmt.Errorf("network %q not found, %w", c.Network, nearest))
return
}

View File

@@ -2,6 +2,7 @@ package docker
import (
"fmt"
"strconv"
"strings"
"github.com/goccy/go-yaml"
@@ -12,6 +13,16 @@ import (
var ErrInvalidLabel = gperr.New("invalid label")
const nsProxyDot = NSProxy + "."
var refPrefixes = func() []string {
prefixes := make([]string, 100)
for i := range prefixes {
prefixes[i] = nsProxyDot + "#" + strconv.Itoa(i+1) + "."
}
return prefixes
}()
func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, gperr.Error) {
nestedMap := make(types.LabelMap)
errs := gperr.NewBuilder("labels error")
@@ -57,57 +68,83 @@ func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, g
}
func ExpandWildcard(labels map[string]string, aliases ...string) {
// collect all explicit aliases first
aliasSet := make(map[string]int, len(labels))
// wildcardLabels holds mapping suffix -> value derived from wildcard label definitions
wildcardLabels := make(map[string]string)
aliasSet := make(map[string]int, len(aliases))
for i, alias := range aliases {
aliasSet[alias] = i
}
// iterate over a copy of the keys to safely mutate the map while ranging
wildcardLabels := make(map[string]string)
// First pass: collect wildcards and discover aliases
for lbl, value := range labels {
parts := strings.SplitN(lbl, ".", 3)
if len(parts) < 2 || parts[0] != NSProxy {
if !strings.HasPrefix(lbl, nsProxyDot) {
continue
}
alias := parts[1]
if alias == WildcardAlias { // "*"
// remove wildcard label from original map it should not remain afterwards
// lbl is "proxy.X..." where X is alias or wildcard
rest := lbl[len(nsProxyDot):] // "X..." or "X.suffix"
dotIdx := strings.IndexByte(rest, '.')
var alias, suffix string
if dotIdx == -1 {
alias = rest
} else {
alias = rest[:dotIdx]
suffix = rest[dotIdx+1:]
}
if alias == WildcardAlias {
delete(labels, lbl)
// value looks like YAML (multiline)
if strings.Count(value, "\n") > 1 {
if suffix == "" || strings.Count(value, "\n") > 1 {
expandYamlWildcard(value, wildcardLabels)
continue
} else {
wildcardLabels[suffix] = value
}
// normal wildcard label with suffix store directly
wildcardLabels[parts[2]] = value
continue
}
// explicit alias label remember the alias (but not reference aliases like #1, #2)
if _, ok := aliasSet[alias]; !ok && !strings.HasPrefix(alias, "#") {
if suffix == "" || alias[0] == '#' {
continue
}
if _, known := aliasSet[alias]; !known {
aliasSet[alias] = len(aliasSet)
}
}
if len(aliasSet) == 0 || len(wildcardLabels) == 0 {
return // nothing to expand
return
}
// expand collected wildcard labels for every alias
for suffix, v := range wildcardLabels {
for alias, i := range aliasSet {
// use numeric index instead of the alias name
alias = fmt.Sprintf("#%d", i+1)
// Second pass: convert explicit labels to #N format
for lbl, value := range labels {
if !strings.HasPrefix(lbl, nsProxyDot) {
continue
}
rest := lbl[len(nsProxyDot):]
dotIdx := strings.IndexByte(rest, '.')
if dotIdx == -1 {
continue
}
alias := rest[:dotIdx]
if alias[0] == '#' {
continue
}
suffix := rest[dotIdx+1:]
key := fmt.Sprintf("%s.%s.%s", NSProxy, alias, suffix)
if suffix == "" { // this should not happen (root wildcard handled earlier) but keep safe
key = fmt.Sprintf("%s.%s", NSProxy, alias)
}
labels[key] = v
idx, known := aliasSet[alias]
if !known {
continue
}
delete(labels, lbl)
if _, overridden := wildcardLabels[suffix]; !overridden {
labels[refPrefixes[idx]+suffix] = value
}
}
// Expand wildcards for all aliases
for suffix, value := range wildcardLabels {
for _, idx := range aliasSet {
labels[refPrefixes[idx]+suffix] = value
}
}
}
@@ -139,12 +176,46 @@ func flattenMap(prefix string, src map[string]any, dest map[string]string) {
case map[string]any:
flattenMap(key, vv, dest)
case map[any]any:
// convert to map[string]any by stringifying keys
tmp := make(map[string]any, len(vv))
for kk, vvv := range vv {
tmp[fmt.Sprintf("%v", kk)] = vvv
}
flattenMap(key, tmp, dest)
flattenMapAny(key, vv, dest)
case string:
dest[key] = vv
case int:
dest[key] = strconv.Itoa(vv)
case bool:
dest[key] = strconv.FormatBool(vv)
case float64:
dest[key] = strconv.FormatFloat(vv, 'f', -1, 64)
default:
dest[key] = fmt.Sprint(v)
}
}
}
func flattenMapAny(prefix string, src map[any]any, dest map[string]string) {
for k, v := range src {
var key string
switch kk := k.(type) {
case string:
key = kk
default:
key = fmt.Sprint(k)
}
if prefix != "" {
key = prefix + "." + key
}
switch vv := v.(type) {
case map[string]any:
flattenMap(key, vv, dest)
case map[any]any:
flattenMapAny(key, vv, dest)
case string:
dest[key] = vv
case int:
dest[key] = strconv.Itoa(vv)
case bool:
dest[key] = strconv.FormatBool(vv)
case float64:
dest[key] = strconv.FormatFloat(vv, 'f', -1, 64)
default:
dest[key] = fmt.Sprint(v)
}

View File

@@ -8,87 +8,248 @@ import (
)
func TestExpandWildcard(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
"proxy.b.scheme": "http",
"proxy.*.port": "5555",
"proxy.*.healthcheck.disable": "true",
}
t.Run("basic", func(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
"proxy.b.scheme": "http",
"proxy.*.port": "5555",
"proxy.*.healthcheck.disable": "true",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.healthcheck.disable": "true",
"proxy.#2.port": "5555",
"proxy.#2.scheme": "http",
"proxy.#2.healthcheck.disable": "true",
}, labels)
})
docker.ExpandWildcard(labels)
t.Run("no wildcards", func(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
}, labels)
})
require.Equal(t, map[string]string{
"proxy.a.host": "localhost",
"proxy.a.port": "5555",
"proxy.a.healthcheck.disable": "true",
"proxy.b.scheme": "http",
"proxy.b.port": "5555",
"proxy.b.healthcheck.disable": "true",
}, labels)
}
t.Run("no aliases", func(t *testing.T) {
labels := map[string]string{
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels)
require.Equal(t, map[string]string{}, labels)
})
func TestExpandWildcardWithFQDNAliases(t *testing.T) {
labels := map[string]string{
"proxy.c.host": "localhost",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a.example.com", "b.example.com")
require.Equal(t, map[string]string{
"proxy.#1.port": "5555",
"proxy.#2.port": "5555",
"proxy.c.host": "localhost",
"proxy.c.port": "5555",
}, labels)
t.Run("empty labels", func(t *testing.T) {
labels := map[string]string{}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{}, labels)
})
t.Run("only wildcards no explicit labels", func(t *testing.T) {
labels := map[string]string{
"proxy.*.port": "5555",
"proxy.*.scheme": "https",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.port": "5555",
"proxy.#1.scheme": "https",
"proxy.#2.port": "5555",
"proxy.#2.scheme": "https",
}, labels)
})
t.Run("non-proxy labels unchanged", func(t *testing.T) {
labels := map[string]string{
"other.label": "value",
"proxy.*.port": "5555",
"proxy.a.scheme": "http",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"other.label": "value",
"proxy.#1.port": "5555",
"proxy.#1.scheme": "http",
}, labels)
})
t.Run("single alias multiple labels", func(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.a.port": "8080",
"proxy.a.scheme": "https",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.scheme": "https",
}, labels)
})
t.Run("wildcard partial override", func(t *testing.T) {
labels := map[string]string{
"proxy.a.host": "localhost",
"proxy.a.port": "8080",
"proxy.a.healthcheck.path": "/health",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.healthcheck.path": "/health",
}, labels)
})
t.Run("nested suffix distinction", func(t *testing.T) {
labels := map[string]string{
"proxy.a.healthcheck.path": "/health",
"proxy.a.healthcheck.interval": "10s",
"proxy.*.healthcheck.disable": "true",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.#1.healthcheck.path": "/health",
"proxy.#1.healthcheck.interval": "10s",
"proxy.#1.healthcheck.disable": "true",
}, labels)
})
t.Run("discovered alias from explicit label", func(t *testing.T) {
labels := map[string]string{
"proxy.c.host": "localhost",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.port": "5555",
"proxy.#2.port": "5555",
"proxy.#3.host": "localhost",
"proxy.#3.port": "5555",
}, labels)
})
t.Run("ref alias not converted", func(t *testing.T) {
labels := map[string]string{
"proxy.#1.host": "localhost",
"proxy.#2.port": "8080",
"proxy.*.scheme": "https",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.scheme": "https",
"proxy.#2.port": "8080",
"proxy.#2.scheme": "https",
}, labels)
})
t.Run("mixed ref and named aliases", func(t *testing.T) {
labels := map[string]string{
"proxy.#1.host": "host1",
"proxy.a.host": "host2",
"proxy.*.port": "5555",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "host2",
"proxy.#1.port": "5555",
"proxy.#2.port": "5555",
}, labels)
})
}
func TestExpandWildcardYAML(t *testing.T) {
yaml := `
t.Run("basic yaml wildcard", func(t *testing.T) {
yaml := `
host: localhost
port: 5555
healthcheck:
disable: true`
labels := map[string]string{
"proxy.*": yaml[1:],
"proxy.a.port": "4444",
"proxy.a.healthcheck.disable": "false",
"proxy.a.healthcheck.path": "/health",
"proxy.b.port": "6666",
}
docker.ExpandWildcard(labels)
require.Equal(t, map[string]string{
"proxy.a.host": "localhost", // set by wildcard
"proxy.a.port": "5555", // overridden by wildcard
"proxy.a.healthcheck.disable": "true", // overridden by wildcard
"proxy.a.healthcheck.path": "/health", // own label
"proxy.b.host": "localhost", // set by wildcard
"proxy.b.port": "5555", // overridden by wildcard
"proxy.b.healthcheck.disable": "true", // overridden by wildcard
}, labels)
}
disable: true`[1:]
labels := map[string]string{
"proxy.*": yaml,
"proxy.a.port": "4444",
"proxy.a.healthcheck.disable": "false",
"proxy.a.healthcheck.path": "/health",
"proxy.b.port": "6666",
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.healthcheck.disable": "true",
"proxy.#1.healthcheck.path": "/health",
"proxy.#2.host": "localhost",
"proxy.#2.port": "5555",
"proxy.#2.healthcheck.disable": "true",
}, labels)
})
func TestWildcardWithRefAliases(t *testing.T) {
labels := map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.*.middlewares.request.hide_headers": "X-Header1,X-Header2",
}
docker.ExpandWildcard(labels, "a.example.com", "b.example.com")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "5555",
"proxy.#1.middlewares.request.hide_headers": "X-Header1,X-Header2",
"proxy.#2.middlewares.request.hide_headers": "X-Header1,X-Header2",
}, labels)
t.Run("yaml with nested maps", func(t *testing.T) {
yaml := `
middlewares:
request:
hide_headers: X-Secret
add_headers:
X-Custom: value`[1:]
labels := map[string]string{
"proxy.*": yaml,
"proxy.a.middlewares.request.set_headers": "X-Override: yes",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.#1.middlewares.request.hide_headers": "X-Secret",
"proxy.#1.middlewares.request.add_headers.X-Custom": "value",
"proxy.#1.middlewares.request.set_headers": "X-Override: yes",
}, labels)
})
t.Run("yaml only no explicit labels", func(t *testing.T) {
yaml := `
host: localhost
port: 8080`[1:]
labels := map[string]string{
"proxy.*": yaml,
}
docker.ExpandWildcard(labels, "a", "b")
require.Equal(t, map[string]string{
"proxy.#1.host": "localhost",
"proxy.#1.port": "8080",
"proxy.#2.host": "localhost",
"proxy.#2.port": "8080",
}, labels)
})
t.Run("invalid yaml ignored", func(t *testing.T) {
labels := map[string]string{
"proxy.*": "invalid: yaml: content:\n\t\tbad",
"proxy.a.port": "8080",
}
docker.ExpandWildcard(labels, "a")
require.Equal(t, map[string]string{
"proxy.a.port": "8080",
}, labels)
})
}
func BenchmarkParseLabels(b *testing.B) {
m := map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
"proxy.*.scheme": "http",
"proxy.*.middlewares.request.hide_headers": "X-Header1,X-Header2",
}
for b.Loop() {
_, _ = docker.ParseLabels(map[string]string{
"proxy.a.host": "localhost",
"proxy.b.port": "4444",
"proxy.*.scheme": "http",
"proxy.*.middlewares.request.hide_headers": "X-Header1,X-Header2",
})
_, _ = docker.ParseLabels(m, "a", "b")
}
}

View File

@@ -82,7 +82,7 @@ func BenchmarkEntrypointReal(b *testing.B) {
Scheme: routeTypes.SchemeHTTP,
Host: host,
Port: route.Port{Proxy: portInt},
HealthCheck: &types.HealthCheckConfig{Disable: true},
HealthCheck: types.HealthCheckConfig{Disable: true},
}
err = r.Validate()
@@ -125,7 +125,7 @@ func BenchmarkEntrypoint(b *testing.B) {
Port: route.Port{
Proxy: 8080,
},
HealthCheck: &types.HealthCheckConfig{
HealthCheck: types.HealthCheckConfig{
Disable: true,
},
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
nettypes "github.com/yusing/godoxy/internal/net/types"
"github.com/yusing/godoxy/internal/utils/pool"
"github.com/yusing/goutils/pool"
)
type route interface {

View File

@@ -9,7 +9,7 @@ import (
"time"
. "github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/utils"
"github.com/yusing/goutils/mockable"
"github.com/yusing/goutils/task"
expect "github.com/yusing/goutils/testing"
)
@@ -57,7 +57,7 @@ func fmtLog(cfg *RequestLoggerConfig) (ts string, line string) {
t := time.Now()
logger := NewMockAccessLogger(testTask, cfg)
utils.MockTimeNow(t)
mockable.MockTimeNow(t)
buf = logger.(RequestFormatter).AppendRequestLog(buf, req, resp)
return t.Format(LogTimeFormat), string(buf)
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/rs/zerolog"
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
"github.com/yusing/godoxy/internal/utils"
"github.com/yusing/goutils/mockable"
)
type (
@@ -67,7 +67,7 @@ func (f *CommonFormatter) AppendRequestLog(line []byte, req *http.Request, res *
line = append(line, clientIP(req)...)
line = append(line, " - - ["...)
line = utils.TimeNow().AppendFormat(line, LogTimeFormat)
line = mockable.TimeNow().AppendFormat(line, LogTimeFormat)
line = append(line, `] "`...)
line = append(line, req.Method...)
@@ -103,7 +103,7 @@ func (f *JSONFormatter) AppendRequestLog(line []byte, req *http.Request, res *ht
writer := bytes.NewBuffer(line)
logger := zerolog.New(writer)
event := logger.Info().
Str("time", utils.TimeNow().Format(LogTimeFormat)).
Str("time", mockable.TimeNow().Format(LogTimeFormat)).
Str("ip", clientIP(req)).
Str("method", req.Method).
Str("scheme", scheme(req)).
@@ -136,7 +136,7 @@ func (f ACLLogFormatter) AppendACLLog(line []byte, info *maxmind.IPInfo, blocked
writer := bytes.NewBuffer(line)
logger := zerolog.New(writer)
event := logger.Info().
Str("time", utils.TimeNow().Format(LogTimeFormat)).
Str("time", mockable.TimeNow().Format(LogTimeFormat)).
Str("ip", info.Str)
if blocked {
event.Str("action", "block")

View File

@@ -8,8 +8,8 @@ import (
"time"
"github.com/rs/zerolog"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/mockable"
strutils "github.com/yusing/goutils/strings"
)
@@ -81,14 +81,14 @@ func rotateLogFile(file supportRotate, config *Retention, result *RotateResult)
func rotateLogFileByPolicy(file supportRotate, config *Retention, result *RotateResult) (rotated bool, err error) {
var shouldStop func() bool
t := utils.TimeNow()
t := mockable.TimeNow()
switch {
case config.Last > 0:
shouldStop = func() bool { return result.NumLinesKeep-result.NumLinesInvalid == int(config.Last) }
// not needed to parse time for last N lines
case config.Days > 0:
cutoff := utils.TimeNow().AddDate(0, 0, -int(config.Days)+1)
cutoff := mockable.TimeNow().AddDate(0, 0, -int(config.Days)+1)
shouldStop = func() bool { return t.Before(cutoff) }
default:
return false, nil // should not happen

View File

@@ -7,7 +7,7 @@ import (
"time"
. "github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/utils"
"github.com/yusing/goutils/mockable"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
expect "github.com/yusing/goutils/testing"
@@ -56,7 +56,7 @@ func TestRotateKeepLast(t *testing.T) {
for _, format := range ReqLoggerFormats {
t.Run(string(format)+" keep last", func(t *testing.T) {
file := NewMockFile(true)
utils.MockTimeNow(testTime)
mockable.MockTimeNow(testTime)
logger := NewAccessLoggerWithIO(task.RootTask("test", false), file, &RequestLoggerConfig{
Format: format,
})
@@ -93,7 +93,7 @@ func TestRotateKeepLast(t *testing.T) {
expect.Nil(t, logger.Config().Retention)
nLines := 10
for i := range nLines {
utils.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
logger.Log(req, resp)
}
logger.Flush()
@@ -105,7 +105,7 @@ func TestRotateKeepLast(t *testing.T) {
expect.Equal(t, retention.KeepSize, 0)
logger.Config().Retention = retention
utils.MockTimeNow(testTime)
mockable.MockTimeNow(testTime)
var result RotateResult
rotated, err := logger.(AccessLogRotater).Rotate(&result)
expect.NoError(t, err)
@@ -139,7 +139,7 @@ func TestRotateKeepFileSize(t *testing.T) {
expect.Nil(t, logger.Config().Retention)
nLines := 10
for i := range nLines {
utils.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
logger.Log(req, resp)
}
logger.Flush()
@@ -151,7 +151,7 @@ func TestRotateKeepFileSize(t *testing.T) {
expect.Equal(t, retention.Last, 0)
logger.Config().Retention = retention
utils.MockTimeNow(testTime)
mockable.MockTimeNow(testTime)
var result RotateResult
rotated, err := logger.(AccessLogRotater).Rotate(&result)
expect.NoError(t, err)
@@ -171,7 +171,7 @@ func TestRotateKeepFileSize(t *testing.T) {
expect.Nil(t, logger.Config().Retention)
nLines := 100
for i := range nLines {
utils.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
logger.Log(req, resp)
}
logger.Flush()
@@ -183,7 +183,7 @@ func TestRotateKeepFileSize(t *testing.T) {
expect.Equal(t, retention.Last, 0)
logger.Config().Retention = retention
utils.MockTimeNow(testTime)
mockable.MockTimeNow(testTime)
var result RotateResult
rotated, err := logger.(AccessLogRotater).Rotate(&result)
expect.NoError(t, err)
@@ -205,7 +205,7 @@ func TestRotateSkipInvalidTime(t *testing.T) {
expect.Nil(t, logger.Config().Retention)
nLines := 10
for i := range nLines {
utils.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -nLines+i+1))
logger.Log(req, resp)
logger.Flush()
@@ -248,7 +248,7 @@ func BenchmarkRotate(b *testing.B) {
Format: FormatJSON,
})
for i := range 100 {
utils.MockTimeNow(testTime.AddDate(0, 0, -100+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -100+i+1))
logger.Log(req, resp)
}
logger.Flush()
@@ -282,7 +282,7 @@ func BenchmarkRotateWithInvalidTime(b *testing.B) {
Format: FormatJSON,
})
for i := range 10000 {
utils.MockTimeNow(testTime.AddDate(0, 0, -10000+i+1))
mockable.MockTimeNow(testTime.AddDate(0, 0, -10000+i+1))
logger.Log(req, resp)
if i%10 == 0 {
_, _ = file.Write([]byte("invalid time\n"))

View File

@@ -178,7 +178,7 @@ func (cfg *MaxMind) doReq(method string) (*http.Response, error) {
if err != nil {
return nil, err
}
req.SetBasicAuth(cfg.AccountID, cfg.LicenseKey)
req.SetBasicAuth(cfg.AccountID, cfg.LicenseKey.String())
resp, err := doReq(req)
if err != nil {
return nil, err

View File

@@ -4,14 +4,15 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
type (
DatabaseType string
Config struct {
AccountID string `json:"account_id" validate:"required"`
LicenseKey string `json:"license_key" validate:"required"`
Database DatabaseType `json:"database" validate:"omitempty,oneof=geolite geoip2"`
AccountID string `json:"account_id" validate:"required"`
LicenseKey strutils.Redacted `json:"license_key" validate:"required"`
Database DatabaseType `json:"database" validate:"omitempty,oneof=geolite geoip2"`
}
)

View File

@@ -10,8 +10,8 @@ import (
"github.com/rs/zerolog/log"
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/utils/pool"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/pool"
"github.com/yusing/goutils/task"
"golang.org/x/sync/errgroup"
)

View File

@@ -7,6 +7,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/route/rules"
httputils "github.com/yusing/goutils/http"
)
type Bypass []rules.RuleOn
@@ -50,7 +51,7 @@ func (c *checkBypass) before(w http.ResponseWriter, r *http.Request) (proceedNex
}
func (c *checkBypass) modifyResponse(resp *http.Response) error {
if c.modRes == nil || (!c.isEnforced(resp.Request) && c.bypass.ShouldBypass(rules.ResponseAsRW(resp), resp.Request)) {
if c.modRes == nil || (!c.isEnforced(resp.Request) && c.bypass.ShouldBypass(httputils.ResponseAsRW(resp), resp.Request)) {
return nil
}
log.Debug().Str("middleware", c.name).Str("url", resp.Request.Host+resp.Request.URL.Path).Msg("modifying response")

View File

@@ -201,6 +201,7 @@ func TestBypassResponse(t *testing.T) {
StatusCode: test.statusCode,
Body: io.NopCloser(strings.NewReader("test")),
Header: make(http.Header),
Request: httptest.NewRequest("GET", "http://example.com", nil),
}
mErr := mr.ModifyResponse(resp)
expect.NoError(t, mErr)

View File

@@ -8,7 +8,6 @@ import (
_ "embed"
"github.com/yusing/godoxy/internal/jsonstore"
"github.com/yusing/godoxy/internal/utils"
)
type CaptchaSession struct {
@@ -22,7 +21,7 @@ var CaptchaSessions = jsonstore.Store[*CaptchaSession]("captcha_sessions")
func newCaptchaSession(p Provider) *CaptchaSession {
buf := make([]byte, 32)
_, _ = rand.Read(buf)
now := utils.TimeNow()
now := time.Now()
return &CaptchaSession{
ID: hex.EncodeToString(buf),
Expiry: now.Add(p.SessionExpiry()),
@@ -30,5 +29,5 @@ func newCaptchaSession(p Provider) *CaptchaSession {
}
func (s *CaptchaSession) expired() bool {
return utils.TimeNow().After(s.Expiry)
return time.Now().After(s.Expiry)
}

View File

@@ -9,10 +9,10 @@ import (
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
"github.com/yusing/godoxy/internal/watcher"
"github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/fs"
"github.com/yusing/goutils/task"
)
@@ -46,7 +46,7 @@ func GetErrorPageByStatus(statusCode int) (content []byte, ok bool) {
}
func loadContent() {
files, err := utils.ListFiles(errPagesBasePath, 0)
files, err := fs.ListFiles(errPagesBasePath, 0)
if err != nil {
log.Err(err).Msg("failed to list error page resources")
return

View File

@@ -9,9 +9,10 @@ import (
"github.com/bytedance/sonic"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/route/rules"
"github.com/yusing/godoxy/internal/serialization"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/reverseproxy"
)
@@ -190,11 +191,22 @@ func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r *
}
}
if exec, ok := m.impl.(ResponseModifier); ok {
rm := rules.NewResponseModifier(w)
defer rm.FlushRelease()
next(rm, r)
if httpheaders.IsWebsocket(r.Header) || r.Header.Get("Accept") == "text/event-stream" {
next(w, r)
return
}
if exec, ok := m.impl.(ResponseModifier); ok {
lrm := httputils.NewLazyResponseModifier(w, needsBuffering)
defer lrm.FlushRelease()
next(lrm, r)
// Skip modification if response wasn't buffered (non-HTML content)
if !lrm.IsBuffered() {
return
}
rm := lrm.ResponseModifier()
currentBody := rm.BodyReader()
currentResp := &http.Response{
StatusCode: rm.StatusCode(),
@@ -210,8 +222,8 @@ func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r *
// override the response status code
rm.WriteHeader(currentResp.StatusCode)
// overriding the response header is not necessary
// modifyResponse is supposed to write to Header directly instead of assigning a new header map)
// overriding the response header
maps.Copy(rm.Header(), currentResp.Header)
// override the content length and body if changed
if currentResp.Body != currentBody {
@@ -222,6 +234,12 @@ func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r *
}
}
// needsBuffering determines if a response should be buffered for modification.
// Only HTML responses need buffering; streaming content (video, audio, etc.) should pass through.
func needsBuffering(header http.Header) bool {
return httputils.GetContentType(header).IsHTML()
}
func (m *Middleware) LogWarn(req *http.Request) *zerolog.Event {
return log.Warn().Str("middleware", m.name).
Str("host", req.Host).

View File

@@ -7,8 +7,8 @@ import (
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
fsutils "github.com/yusing/goutils/fs"
strutils "github.com/yusing/goutils/strings"
)
@@ -52,7 +52,7 @@ func Get(name string) (*Middleware, Error) {
if !ok {
return nil, ErrUnknownMiddleware.
Subject(name).
With(gperr.DoYouMean(utils.NearestField(name, allMiddlewares)))
With(gperr.DoYouMeanField(name, allMiddlewares))
}
return middleware, nil
}
@@ -63,7 +63,7 @@ func All() map[string]*Middleware {
func LoadComposeFiles() {
errs := gperr.NewBuilder("middleware compile errors")
middlewareDefs, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0)
middlewareDefs, err := fsutils.ListFiles(common.MiddlewareComposeBasePath, 0)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return

View File

@@ -40,6 +40,10 @@ func (eofReader) Close() error { return nil }
// modifyResponse implements ResponseModifier.
func (m *modifyHTML) modifyResponse(resp *http.Response) error {
// Skip HEAD requests - no body to modify
if resp.Request.Method == http.MethodHead {
return nil
}
// including text/html and application/xhtml+xml
if !httputils.GetContentType(resp.Header).IsHTML() {
return nil

View File

@@ -4,11 +4,9 @@ import (
urlPkg "net/url"
"github.com/bytedance/sonic"
"github.com/yusing/godoxy/internal/utils"
)
type URL struct {
_ utils.NoCopy
urlPkg.URL
}

View File

@@ -11,13 +11,14 @@ import (
"github.com/luthermonson/go-proxmox"
"github.com/yusing/godoxy/internal/net/gphttp"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
type Config struct {
URL string `json:"url" validate:"required,url"`
TokenID string `json:"token_id" validate:"required"`
Secret string `json:"secret" validate:"required"`
TokenID string `json:"token_id" validate:"required"`
Secret strutils.Redacted `json:"secret" validate:"required"`
NoTLSVerify bool `json:"no_tls_verify" yaml:"no_tls_verify,omitempty"`
@@ -48,7 +49,7 @@ func (c *Config) Init() gperr.Error {
}
opts := []proxmox.Option{
proxmox.WithAPIToken(c.TokenID, c.Secret),
proxmox.WithAPIToken(c.TokenID, c.Secret.String()),
proxmox.WithHTTPClient(&http.Client{
Transport: tr,
}),

View File

@@ -7,7 +7,7 @@ import (
"github.com/bytedance/sonic"
"github.com/luthermonson/go-proxmox"
"github.com/yusing/godoxy/internal/utils/pool"
"github.com/yusing/goutils/pool"
)
type Node struct {

View File

@@ -9,6 +9,7 @@ import (
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/watcher/health/monitor"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/task"
@@ -24,6 +25,8 @@ type (
}
)
var _ types.FileServerRoute = (*FileServer)(nil)
func handler(root string) http.Handler {
return http.FileServer(http.Dir(root))
}
@@ -91,16 +94,12 @@ func (s *FileServer) Start(parent task.Parent) gperr.Error {
}
if s.UseHealthCheck() {
s.HealthMon = monitor.NewFileServerHealthMonitor(s.HealthCheck, s.Root)
s.HealthMon = monitor.NewMonitor(s)
if err := s.HealthMon.Start(s.task); err != nil {
return err
}
}
if s.ShouldExclude() {
return nil
}
routes.HTTP.Add(s)
s.task.OnFinished("remove_route_from_http", func() {
routes.HTTP.Del(s)
@@ -108,6 +107,10 @@ func (s *FileServer) Start(parent task.Parent) gperr.Error {
return nil
}
func (s *FileServer) RootPath() string {
return s.Root
}
// ServeHTTP implements http.Handler.
func (s *FileServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
s.handler.ServeHTTP(w, req)

View File

@@ -224,10 +224,13 @@ func (p *Provider) startRoute(parent task.Parent, r *route.Route) gperr.Error {
p.lockDeleteRoute(r.Alias)
return err.Subject(r.Alias)
}
p.lockAddRoute(r)
r.Task().OnCancel("remove_route_from_provider", func() {
p.lockDeleteRoute(r.Alias)
})
if !r.ShouldExclude() {
r.Task().OnCancel("remove_route_from_provider", func() {
p.lockDeleteRoute(r.Alias)
})
}
return nil
}

View File

@@ -137,10 +137,6 @@ func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error {
}
}
if r.ShouldExclude() {
return nil
}
if r.UseLoadBalance() {
r.addToLoadBalancer(parent)
} else {

View File

@@ -15,6 +15,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/homepage"
homepagecfg "github.com/yusing/godoxy/internal/homepage/types"
@@ -49,7 +50,7 @@ type (
PathPatterns []string `json:"path_patterns,omitempty" extensions:"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,omitempty" extensions:"x-nullable"` // null on load-balancer routes
HealthCheck types.HealthCheckConfig `json:"healthcheck,omitempty" extensions:"x-nullable"` // null on load-balancer routes
LoadBalance *types.LoadBalancerConfig `json:"load_balance,omitempty" extensions:"x-nullable"`
Middlewares map[string]types.LabelMap `json:"middlewares,omitempty" extensions:"x-nullable"`
Homepage *homepage.ItemConfig `json:"homepage"`
@@ -378,7 +379,8 @@ func (r *Route) start(parent task.Parent) gperr.Error {
defer close(r.started)
// skip checking for excluded routes
if !r.ShouldExclude() {
excluded := r.ShouldExclude()
if !excluded {
if err := checkExists(r); err != nil {
return err
}
@@ -388,8 +390,12 @@ func (r *Route) start(parent task.Parent) gperr.Error {
docker.SetDockerHostByContainerID(cont.ContainerID, cont.DockerHost)
}
if err := r.impl.Start(parent); err != nil {
return err
if !excluded {
if err := r.impl.Start(parent); err != nil {
return err
}
} else { // required by idlewatcher
r.task = parent.Subtask("excluded."+r.Name(), false)
}
return nil
}
@@ -494,7 +500,7 @@ func (r *Route) IdlewatcherConfig() *types.IdlewatcherConfig {
return r.Idlewatcher
}
func (r *Route) HealthCheckConfig() *types.HealthCheckConfig {
func (r *Route) HealthCheckConfig() types.HealthCheckConfig {
return r.HealthCheck
}
@@ -577,30 +583,10 @@ func (r *Route) IsZeroPort() bool {
}
func (r *Route) ShouldExclude() bool {
if r.valErr.Get() != nil {
if r.ExcludedReason != ExcludedReasonNone {
return true
}
if r.Excluded {
return true
}
if r.Container != nil {
switch {
case r.Container.IsExcluded:
return true
case r.IsZeroPort() && !r.UseIdleWatcher():
return true
case !r.Container.IsExplicit && docker.IsBlacklisted(r.Container):
return true
case strings.HasPrefix(r.Container.ContainerName, "buildx_"):
return true
}
} else if r.IsZeroPort() && r.Scheme != route.SchemeFileServer {
return true
}
if strings.HasSuffix(r.Alias, "-old") {
return true
}
return false
return r.findExcludedReason() != ExcludedReasonNone
}
type ExcludedReason uint8
@@ -788,17 +774,7 @@ func (r *Route) Finalize() {
}
r.Port.Listening, r.Port.Proxy = lp, pp
if r.HealthCheck == nil {
r.HealthCheck = types.DefaultHealthConfig()
}
if r.HealthCheck.Interval == 0 {
r.HealthCheck.Interval = common.HealthCheckIntervalDefault
}
if r.HealthCheck.Timeout == 0 {
r.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
}
r.HealthCheck.ApplyDefaults(config.ActiveConfig.Load().Defaults.HealthCheck)
}
func (r *Route) FinalizeHomepageConfig() {
@@ -811,7 +787,6 @@ func (r *Route) FinalizeHomepageConfig() {
if r.Homepage == nil {
r.Homepage = &homepage.ItemConfig{
Show: true,
Name: r.Alias,
}
}

View File

@@ -2,6 +2,7 @@ package route
import (
"testing"
"time"
"github.com/yusing/godoxy/internal/common"
route "github.com/yusing/godoxy/internal/route/types"
@@ -40,7 +41,7 @@ func TestRouteValidate(t *testing.T) {
Scheme: route.SchemeHTTP,
Host: "example.com",
Port: route.Port{Proxy: 80},
HealthCheck: &types.HealthCheckConfig{
HealthCheck: types.HealthCheckConfig{
Disable: true,
},
LoadBalance: &types.LoadBalancerConfig{
@@ -179,3 +180,14 @@ func TestRouteAgent(t *testing.T) {
expect.NoError(t, err, "Validate should not return error for valid route with agent")
expect.NotNil(t, r.GetAgent(), "GetAgent should return agent")
}
func TestRouteApplyingHealthCheckDefaults(t *testing.T) {
hc := types.HealthCheckConfig{}
hc.ApplyDefaults(types.HealthCheckConfig{
Interval: 15 * time.Second,
Timeout: 10 * time.Second,
})
expect.Equal(t, hc.Interval, 15*time.Second)
expect.Equal(t, hc.Timeout, 10*time.Second)
}

View File

@@ -2,10 +2,6 @@ package routes
import (
"context"
"crypto/tls"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"reflect"
@@ -34,7 +30,8 @@ func (r *RouteContext) Value(key any) any {
func WithRouteContext(r *http.Request, route types.HTTPRoute) *http.Request {
// we don't want to copy the request object every fucking requests
// return r.WithContext(context.WithValue(r.Context(), routeContextKey, route))
(*requestInternal)(unsafe.Pointer(r)).ctx = &RouteContext{
ctxFieldPtr := (*context.Context)(unsafe.Pointer(uintptr(unsafe.Pointer(r)) + ctxFieldOffset))
*ctxFieldPtr = &RouteContext{
Context: r.Context(),
Route: route,
}
@@ -107,43 +104,12 @@ func TryGetUpstreamURL(r *http.Request) string {
return ""
}
type requestInternal struct {
Method string
URL *url.URL
Proto string
ProtoMajor int
ProtoMinor int
Header http.Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error)
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
Trailer http.Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
Cancel <-chan struct{}
Response *http.Response
Pattern string
ctx context.Context
}
var ctxFieldOffset uintptr
func init() {
// make sure ctx has the same offset as http.Request
f, ok := reflect.TypeFor[requestInternal]().FieldByName("ctx")
f, ok := reflect.TypeFor[http.Request]().FieldByName("ctx")
if !ok {
panic("ctx field not found")
}
f2, ok := reflect.TypeFor[http.Request]().FieldByName("ctx")
if !ok {
panic("ctx field not found")
}
if f.Offset != f2.Offset {
panic(fmt.Sprintf("ctx has different offset than http.Request: %d != %d", f.Offset, f2.Offset))
}
ctxFieldOffset = f.Offset
}

View File

@@ -2,7 +2,7 @@ package routes
import (
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/utils/pool"
"github.com/yusing/goutils/pool"
)
var (

View File

@@ -1,108 +0,0 @@
package rules
import (
"net"
"net/http"
"net/url"
"sync"
)
// Cache is a map of cached values for a request.
// It prevents the same value from being parsed multiple times.
type (
Cache map[string]any
UpdateFunc[T any] func(T) T
)
const (
cacheKeyQueries = "queries"
cacheKeyCookies = "cookies"
cacheKeyRemoteIP = "remote_ip"
cacheKeyBasicAuth = "basic_auth"
)
var cachePool = sync.Pool{
New: func() any {
return make(Cache)
},
}
// NewCache returns a new Cached.
func NewCache() Cache {
return cachePool.Get().(Cache)
}
// Release clear the contents of the Cached and returns it to the pool.
func (c Cache) Release() {
clear(c)
cachePool.Put(c)
}
// GetQueries returns the queries.
// If r does not have queries, an empty map is returned.
func (c Cache) GetQueries(r *http.Request) url.Values {
v, ok := c[cacheKeyQueries]
if !ok {
v = r.URL.Query()
c[cacheKeyQueries] = v
}
return v.(url.Values)
}
func (c Cache) UpdateQueries(r *http.Request, update func(url.Values)) {
queries := c.GetQueries(r)
update(queries)
r.URL.RawQuery = queries.Encode()
}
// GetCookies returns the cookies.
// If r does not have cookies, an empty slice is returned.
func (c Cache) GetCookies(r *http.Request) []*http.Cookie {
v, ok := c[cacheKeyCookies]
if !ok {
v = r.Cookies()
c[cacheKeyCookies] = v
}
return v.([]*http.Cookie)
}
func (c Cache) UpdateCookies(r *http.Request, update UpdateFunc[[]*http.Cookie]) {
cookies := update(c.GetCookies(r))
c[cacheKeyCookies] = cookies
r.Header.Del("Cookie")
for _, cookie := range cookies {
r.AddCookie(cookie)
}
}
// GetRemoteIP returns the remote ip address.
// If r.RemoteAddr is not a valid ip address, nil is returned.
func (c Cache) GetRemoteIP(r *http.Request) net.IP {
v, ok := c[cacheKeyRemoteIP]
if !ok {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host = r.RemoteAddr
}
v = net.ParseIP(host)
c[cacheKeyRemoteIP] = v
}
return v.(net.IP)
}
// GetBasicAuth returns *Credentials the basic auth username and password.
// If r does not have basic auth, nil is returned.
func (c Cache) GetBasicAuth(r *http.Request) *Credentials {
v, ok := c[cacheKeyBasicAuth]
if !ok {
u, p, ok := r.BasicAuth()
if ok {
v = &Credentials{u, []byte(p)}
c[cacheKeyBasicAuth] = v
} else {
c[cacheKeyBasicAuth] = nil
return nil
}
}
return v.(*Credentials)
}

View File

@@ -1,16 +1,15 @@
package rules
import "golang.org/x/crypto/bcrypt"
import (
httputils "github.com/yusing/goutils/http"
"golang.org/x/crypto/bcrypt"
)
type (
HashedCrendentials struct {
Username string
CheckMatch func(inputPwd []byte) bool
}
Credentials struct {
Username string
Password []byte
}
)
func BCryptCrendentials(username string, hashedPassword []byte) *HashedCrendentials {
@@ -19,7 +18,7 @@ func BCryptCrendentials(username string, hashedPassword []byte) *HashedCrendenti
}}
}
func (hc *HashedCrendentials) Match(cred *Credentials) bool {
func (hc *HashedCrendentials) Match(cred *httputils.Credentials) bool {
if cred == nil {
return false
}

View File

@@ -10,7 +10,6 @@ import (
"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"
nettypes "github.com/yusing/godoxy/internal/net/types"
@@ -50,6 +49,14 @@ const (
CommandPassAlt = "bypass"
)
type AuthHandler func(w http.ResponseWriter, r *http.Request) (proceed bool)
var authHandler AuthHandler
func InitAuthHandler(handler AuthHandler) {
authHandler = handler
}
var commands = map[string]struct {
help Help
validate ValidateFunc
@@ -70,7 +77,7 @@ var commands = map[string]struct {
},
build: func(args any) CommandHandler {
return NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
if !auth.AuthOrProceed(w, r) {
if !authHandler(w, r) {
return errTerminated
}
return nil
@@ -198,7 +205,7 @@ var commands = map[string]struct {
code, textTmpl := args.(*Tuple[int, templateString]).Unpack()
return TerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
// error command should overwrite the response body
GetInitResponseModifier(w).ResetBody()
httputils.GetInitResponseModifier(w).ResetBody()
w.WriteHeader(code)
err := textTmpl.ExpandVars(w, r, w)
return err

View File

@@ -7,6 +7,7 @@ import (
"strconv"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http"
ioutils "github.com/yusing/goutils/io"
)
@@ -128,7 +129,7 @@ var modFields = map[string]struct {
if err != nil {
return err
}
GetSharedData(w).UpdateQueries(r, func(queries url.Values) {
httputils.GetSharedData(w).UpdateQueries(r, func(queries url.Values) {
queries.Set(k, v)
})
return nil
@@ -138,13 +139,13 @@ var modFields = map[string]struct {
if err != nil {
return err
}
GetSharedData(w).UpdateQueries(r, func(queries url.Values) {
httputils.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) {
httputils.GetSharedData(w).UpdateQueries(r, func(queries url.Values) {
queries.Del(k)
})
return nil
@@ -169,7 +170,7 @@ var modFields = map[string]struct {
if err != nil {
return err
}
GetSharedData(w).UpdateCookies(r, func(cookies []*http.Cookie) []*http.Cookie {
httputils.GetSharedData(w).UpdateCookies(r, func(cookies []*http.Cookie) []*http.Cookie {
for i, c := range cookies {
if c.Name == k {
cookies[i].Value = v
@@ -185,13 +186,13 @@ var modFields = map[string]struct {
if err != nil {
return err
}
GetSharedData(w).UpdateCookies(r, func(cookies []*http.Cookie) []*http.Cookie {
httputils.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 {
httputils.GetSharedData(w).UpdateCookies(r, func(cookies []*http.Cookie) []*http.Cookie {
index := -1
for i, c := range cookies {
if c.Name == k {
@@ -242,7 +243,7 @@ var modFields = map[string]struct {
r.Body = nil
}
bufPool := GetInitResponseModifier(w).BufPool()
bufPool := httputils.GetInitResponseModifier(w).BufPool()
b := bufPool.GetBuffer()
err := tmpl.ExpandVars(w, r, b)
if err != nil {
@@ -282,7 +283,7 @@ var modFields = map[string]struct {
tmpl := args.(templateString)
return &FieldHandler{
set: OnResponseCommand(func(w http.ResponseWriter, r *http.Request) error {
rm := GetInitResponseModifier(w)
rm := httputils.GetInitResponseModifier(w)
rm.ResetBody()
return tmpl.ExpandVars(w, r, rm)
}),
@@ -317,7 +318,7 @@ var modFields = map[string]struct {
status := args.(int)
return &FieldHandler{
set: NonTerminatingCommand(func(w http.ResponseWriter, r *http.Request) error {
GetInitResponseModifier(w).WriteHeader(status)
httputils.GetInitResponseModifier(w).WriteHeader(status)
return nil
}),
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
httputils "github.com/yusing/goutils/http"
)
func TestFieldHandler_Header(t *testing.T) {
@@ -420,7 +421,7 @@ func TestFieldHandler_ResponseBody(t *testing.T) {
name string
template string
setup func(*http.Request)
verify func(*ResponseModifier)
verify func(*httputils.ResponseModifier)
}{
{
name: "set response body with template",
@@ -429,8 +430,8 @@ func TestFieldHandler_ResponseBody(t *testing.T) {
r.Method = "GET"
r.URL.Path = "/api/test"
},
verify: func(rm *ResponseModifier) {
content := rm.buf.String()
verify: func(rm *httputils.ResponseModifier) {
content := string(rm.Content())
expected := "Response: GET /api/test"
assert.Equal(t, expected, content, "Expected response body")
},
@@ -444,7 +445,7 @@ func TestFieldHandler_ResponseBody(t *testing.T) {
w := httptest.NewRecorder()
// Create ResponseModifier wrapper
rm := NewResponseModifier(w)
rm := httputils.NewResponseModifier(w)
tmpl, tErr := validateTemplate(tt.template, false)
if tErr != nil {
@@ -495,7 +496,7 @@ func TestFieldHandler_StatusCode(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
rm := NewResponseModifier(w)
rm := httputils.NewResponseModifier(w)
var cmd Command
err := cmd.Parse(fmt.Sprintf("set %s %d", FieldStatusCode, tt.status))
if err != nil {

View File

@@ -8,6 +8,7 @@ import (
"github.com/yusing/godoxy/internal/route/routes"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http"
)
type RuleOn struct {
@@ -95,11 +96,11 @@ var checkers = map[string]struct {
k, matcher := args.(*MapValueMatcher).Unpack()
if matcher == nil {
return func(w http.ResponseWriter, r *http.Request) bool {
return len(GetInitResponseModifier(w).Header()[k]) > 0
return len(httputils.GetInitResponseModifier(w).Header()[k]) > 0
}
}
return func(w http.ResponseWriter, r *http.Request) bool {
return slices.ContainsFunc(GetInitResponseModifier(w).Header()[k], matcher)
return slices.ContainsFunc(httputils.GetInitResponseModifier(w).Header()[k], matcher)
}
},
},
@@ -122,11 +123,11 @@ var checkers = map[string]struct {
k, matcher := args.(*MapValueMatcher).Unpack()
if matcher == nil {
return func(w http.ResponseWriter, r *http.Request) bool {
return len(GetSharedData(w).GetQueries(r)[k]) > 0
return len(httputils.GetSharedData(w).GetQueries(r)[k]) > 0
}
}
return func(w http.ResponseWriter, r *http.Request) bool {
return slices.ContainsFunc(GetSharedData(w).GetQueries(r)[k], matcher)
return slices.ContainsFunc(httputils.GetSharedData(w).GetQueries(r)[k], matcher)
}
},
},
@@ -149,7 +150,7 @@ var checkers = map[string]struct {
k, matcher := args.(*MapValueMatcher).Unpack()
if matcher == nil {
return func(w http.ResponseWriter, r *http.Request) bool {
cookies := GetSharedData(w).GetCookies(r)
cookies := httputils.GetSharedData(w).GetCookies(r)
for _, cookie := range cookies {
if cookie.Name == k {
return true
@@ -159,7 +160,7 @@ var checkers = map[string]struct {
}
}
return func(w http.ResponseWriter, r *http.Request) bool {
cookies := GetSharedData(w).GetCookies(r)
cookies := httputils.GetSharedData(w).GetCookies(r)
for _, cookie := range cookies {
if cookie.Name == k {
if matcher(cookie.Value) {
@@ -302,7 +303,7 @@ var checkers = map[string]struct {
if ones, bits := ipnet.Mask.Size(); ones == bits {
wantIP := ipnet.IP
return func(w http.ResponseWriter, r *http.Request) bool {
ip := GetSharedData(w).GetRemoteIP(r)
ip := httputils.GetSharedData(w).GetRemoteIP(r)
if ip == nil {
return false
}
@@ -310,7 +311,7 @@ var checkers = map[string]struct {
}
}
return func(w http.ResponseWriter, r *http.Request) bool {
ip := GetSharedData(w).GetRemoteIP(r)
ip := httputils.GetSharedData(w).GetRemoteIP(r)
if ip == nil {
return false
}
@@ -330,7 +331,7 @@ var checkers = map[string]struct {
builder: func(args any) CheckFunc {
cred := args.(*HashedCrendentials)
return func(w http.ResponseWriter, r *http.Request) bool {
return cred.Match(GetSharedData(w).GetBasicAuth(r))
return cred.Match(httputils.GetSharedData(w).GetBasicAuth(r))
}
},
},
@@ -378,11 +379,11 @@ var checkers = map[string]struct {
beg, end := args.(*IntTuple).Unpack()
if beg == end {
return func(w http.ResponseWriter, _ *http.Request) bool {
return GetInitResponseModifier(w).StatusCode() == beg
return httputils.GetInitResponseModifier(w).StatusCode() == beg
}
}
return func(w http.ResponseWriter, _ *http.Request) bool {
statusCode := GetInitResponseModifier(w).StatusCode()
statusCode := httputils.GetInitResponseModifier(w).StatusCode()
return statusCode >= beg && statusCode <= end
}
},

View File

@@ -1,240 +0,0 @@
package rules
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
"github.com/rs/zerolog/log"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/synk"
)
type ResponseModifier struct {
bufPool synk.UnsizedBytesPool
w http.ResponseWriter
buf *bytes.Buffer
statusCode int
shared Cache
hijacked bool
errs gperr.Builder
}
type Response struct {
StatusCode int
Header http.Header
}
func unwrapResponseModifier(w http.ResponseWriter) *ResponseModifier {
for {
switch ww := w.(type) {
case *ResponseModifier:
return ww
case interface{ Unwrap() http.ResponseWriter }:
w = ww.Unwrap()
default:
return nil
}
}
}
type responseAsRW struct {
resp *http.Response
}
func (r responseAsRW) WriteHeader(code int) {
log.Error().Msg("write header after response has been created")
}
func (r responseAsRW) Write(b []byte) (int, error) {
return 0, io.ErrClosedPipe
}
func (r responseAsRW) Header() http.Header {
return r.resp.Header
}
func ResponseAsRW(resp *http.Response) *ResponseModifier {
return &ResponseModifier{
statusCode: resp.StatusCode,
w: responseAsRW{resp},
}
}
// GetInitResponseModifier returns the response modifier for the given response writer.
// If the response writer is already wrapped, it will return the wrapped response modifier.
// Otherwise, it will return a new response modifier.
func GetInitResponseModifier(w http.ResponseWriter) *ResponseModifier {
if rm := unwrapResponseModifier(w); rm != nil {
return rm
}
return NewResponseModifier(w)
}
// GetSharedData returns the shared data for the given response writer.
// It will initialize the shared data if not initialized.
func GetSharedData(w http.ResponseWriter) Cache {
rm := GetInitResponseModifier(w)
if rm.shared == nil {
rm.shared = NewCache()
}
return rm.shared
}
// NewResponseModifier returns a new response modifier for the given response writer.
//
// It should only be called once, at the very beginning of the request.
func NewResponseModifier(w http.ResponseWriter) *ResponseModifier {
return &ResponseModifier{
bufPool: synk.GetUnsizedBytesPool(),
w: w,
}
}
func (rm *ResponseModifier) BufPool() synk.UnsizedBytesPool {
return rm.bufPool
}
// func (rm *ResponseModifier) Unwrap() http.ResponseWriter {
// return rm.w
// }
func (rm *ResponseModifier) WriteHeader(code int) {
rm.statusCode = code
}
// BodyReader returns a reader for the response body.
// Every call to this function will return a new reader that starts from the beginning of the buffer.
func (rm *ResponseModifier) BodyReader() io.ReadCloser {
if rm.buf == nil {
return io.NopCloser(bytes.NewReader(nil))
}
return io.NopCloser(bytes.NewReader(rm.buf.Bytes()))
}
func (rm *ResponseModifier) ResetBody() {
if rm.buf == nil {
return
}
rm.buf.Reset()
}
func (rm *ResponseModifier) SetBody(r io.ReadCloser) error {
if rm.buf == nil {
rm.buf = rm.bufPool.GetBuffer()
} else {
rm.buf.Reset()
}
_, err := io.Copy(rm.buf, r)
if err != nil {
return fmt.Errorf("failed to copy body: %w", err)
}
r.Close()
return nil
}
func (rm *ResponseModifier) ContentLength() int {
if rm.buf == nil {
return 0
}
return rm.buf.Len()
}
func (rm *ResponseModifier) Content() []byte {
if rm.buf == nil {
return nil
}
return rm.buf.Bytes()
}
func (rm *ResponseModifier) StatusCode() int {
if rm.statusCode == 0 {
return http.StatusOK
}
return rm.statusCode
}
func (rm *ResponseModifier) Header() http.Header {
return rm.w.Header()
}
func (rm *ResponseModifier) Response() Response {
return Response{StatusCode: rm.StatusCode(), Header: rm.Header()}
}
func (rm *ResponseModifier) Write(b []byte) (int, error) {
if rm.buf == nil {
rm.buf = rm.bufPool.GetBuffer()
}
return rm.buf.Write(b)
}
// AppendError appends an error to the response modifier
// the error will be formatted as "rule <rule.Name> error: <err>"
//
// It will be aggregated and returned in FlushRelease.
func (rm *ResponseModifier) AppendError(rule Rule, err error) {
rm.errs.Addf("rule %q error: %w", rule.Name, err)
}
func (rm *ResponseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hijacker, ok := rm.w.(http.Hijacker); ok {
rm.hijacked = true
return hijacker.Hijack()
}
return nil, nil, errors.New("hijack not supported")
}
// FlushRelease flushes the response modifier and releases the resources
// it returns the number of bytes written and the aggregated error
// if there is any error (rule errors or write error), it will be returned
func (rm *ResponseModifier) FlushRelease() (int, error) {
n := 0
if !rm.hijacked {
h := rm.w.Header()
// for k := range h {
// if strings.EqualFold(k, "content-length") {
// h.Del(k)
// }
// }
contentLength := rm.ContentLength()
h.Set("Content-Length", strconv.Itoa(rm.ContentLength()))
h.Del("Transfer-Encoding")
h.Del("Trailer")
rm.w.WriteHeader(rm.StatusCode())
if contentLength > 0 {
nn, werr := rm.w.Write(rm.Content())
n += nn
if werr != nil {
rm.errs.Addf("write error: %w", werr)
}
if err := http.NewResponseController(rm.w).Flush(); err != nil && !errors.Is(err, http.ErrNotSupported) {
rm.errs.Addf("flush error: %w", err)
}
}
}
// release the buffer and reset the pointers
if rm.buf != nil {
rm.bufPool.PutBuffer(rm.buf)
rm.buf = nil
}
// release the shared data
if rm.shared != nil {
rm.shared.Release()
rm.shared = nil
}
return n, rm.errs.Error()
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/quic-go/quic-go/http3"
"github.com/rs/zerolog/log"
httputils "github.com/yusing/goutils/http"
"golang.org/x/net/http2"
_ "unsafe"
@@ -91,7 +92,7 @@ func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
}
if defaultRule.IsResponseRule() {
return func(w http.ResponseWriter, r *http.Request) {
rm := NewResponseModifier(w)
rm := httputils.NewResponseModifier(w)
defer func() {
if _, err := rm.FlushRelease(); err != nil {
logError(err, r)
@@ -101,12 +102,12 @@ func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
up(w, r)
err := defaultRule.Do.exec.Handle(w, r)
if err != nil && !errors.Is(err, errTerminated) {
rm.AppendError(defaultRule, err)
appendRuleError(rm, &defaultRule, err)
}
}
}
return func(w http.ResponseWriter, r *http.Request) {
rm := NewResponseModifier(w)
rm := httputils.NewResponseModifier(w)
defer func() {
if _, err := rm.FlushRelease(); err != nil {
logError(err, r)
@@ -119,7 +120,7 @@ func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
return
}
if !errors.Is(err, errTerminated) {
rm.AppendError(defaultRule, err)
appendRuleError(rm, &defaultRule, err)
}
}
}
@@ -138,7 +139,7 @@ func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
defaultTerminates := isTerminatingHandler(defaultRule.Do.exec)
return func(w http.ResponseWriter, r *http.Request) {
rm := NewResponseModifier(w)
rm := httputils.NewResponseModifier(w)
defer func() {
if _, err := rm.FlushRelease(); err != nil {
logError(err, r)
@@ -157,7 +158,7 @@ func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
err := defaultRule.Handle(w, r)
if err != nil {
if !errors.Is(err, errTerminated) {
rm.AppendError(defaultRule, err)
appendRuleError(rm, &defaultRule, err)
}
shouldCallUpstream = false
}
@@ -174,7 +175,7 @@ func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
err := rule.Handle(w, r)
if err != nil {
if !errors.Is(err, errTerminated) {
rm.AppendError(rule, err)
appendRuleError(rm, &rule, err)
}
shouldCallUpstream = false
break
@@ -190,7 +191,7 @@ func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
err := defaultRule.Handle(w, r)
if err != nil {
if !errors.Is(err, errTerminated) {
rm.AppendError(defaultRule, err)
appendRuleError(rm, &defaultRule, err)
return
}
shouldCallUpstream = false
@@ -212,7 +213,7 @@ func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
err := rule.Handle(w, r)
if err != nil {
if !errors.Is(err, errTerminated) {
rm.AppendError(rule, err)
appendRuleError(rm, &rule, err)
}
return
}
@@ -222,12 +223,16 @@ func (rules Rules) BuildHandler(up http.HandlerFunc) http.HandlerFunc {
if isDefaultRulePost {
err := defaultRule.Handle(w, r)
if err != nil && !errors.Is(err, errTerminated) {
rm.AppendError(defaultRule, err)
appendRuleError(rm, &defaultRule, err)
}
}
}
}
func appendRuleError(rm *httputils.ResponseModifier, rule *Rule, err error) {
rm.AppendError("rule: %s, error: %w", rule.Name, err)
}
func isTerminatingHandler(handler CommandHandler) bool {
switch h := handler.(type) {
case TerminatingCommand:

View File

@@ -5,6 +5,8 @@ import (
"net/http"
"strings"
"unsafe"
httputils "github.com/yusing/goutils/http"
)
type templateString struct {
@@ -27,7 +29,7 @@ func (tmpl *templateString) ExpandVars(w http.ResponseWriter, req *http.Request,
return err
}
return ExpandVars(GetInitResponseModifier(w), req, tmpl.string, dstW)
return ExpandVars(httputils.GetInitResponseModifier(w), req, tmpl.string, dstW)
}
func (tmpl *templateString) ExpandVarsToString(w http.ResponseWriter, req *http.Request) (string, error) {
@@ -36,7 +38,7 @@ func (tmpl *templateString) ExpandVarsToString(w http.ResponseWriter, req *http.
}
var buf strings.Builder
err := ExpandVars(GetInitResponseModifier(w), req, tmpl.string, &buf)
err := ExpandVars(httputils.GetInitResponseModifier(w), req, tmpl.string, &buf)
if err != nil {
return "", err
}

View File

@@ -5,10 +5,12 @@ import (
"net/http/httptest"
"net/url"
"testing"
httputils "github.com/yusing/goutils/http"
)
func BenchmarkExpandVars(b *testing.B) {
testResponseModifier := NewResponseModifier(httptest.NewRecorder())
testResponseModifier := httputils.NewResponseModifier(httptest.NewRecorder())
testResponseModifier.WriteHeader(200)
testResponseModifier.Write([]byte("Hello, world!"))
testRequest := httptest.NewRequest("GET", "/", nil)

View File

@@ -8,6 +8,7 @@ import (
"regexp"
"strings"
httputils "github.com/yusing/goutils/http"
ioutils "github.com/yusing/goutils/io"
)
@@ -15,7 +16,7 @@ import (
type (
reqVarGetter func(*http.Request) string
respVarGetter func(*ResponseModifier) string
respVarGetter func(*httputils.ResponseModifier) string
)
var reVar = regexp.MustCompile(`\$[\w_]+`)
@@ -36,7 +37,7 @@ func NeedExpandVars(s string) bool {
}
var (
voidResponseModifier = NewResponseModifier(httptest.NewRecorder())
voidResponseModifier = httputils.NewResponseModifier(httptest.NewRecorder())
dummyRequest = http.Request{
Method: "GET",
URL: &url.URL{Path: "/"},
@@ -50,7 +51,7 @@ func ValidateVars(s string) error {
return ExpandVars(voidResponseModifier, &dummyRequest, s, io.Discard)
}
func ExpandVars(w *ResponseModifier, req *http.Request, src string, dstW io.Writer) error {
func ExpandVars(w *httputils.ResponseModifier, req *http.Request, src string, dstW io.Writer) error {
dst := ioutils.NewBufferedWriter(dstW, 1024)
defer dst.Close()

View File

@@ -4,6 +4,8 @@ import (
"net/http"
"net/url"
"strconv"
httputils "github.com/yusing/goutils/http"
)
var (
@@ -14,31 +16,31 @@ var (
VarPostForm = "postform"
)
type dynamicVarGetter func(args []string, w *ResponseModifier, req *http.Request) (string, error)
type dynamicVarGetter func(args []string, w *httputils.ResponseModifier, req *http.Request) (string, error)
var dynamicVarSubsMap = map[string]dynamicVarGetter{
VarHeader: func(args []string, w *ResponseModifier, req *http.Request) (string, error) {
VarHeader: func(args []string, w *httputils.ResponseModifier, req *http.Request) (string, error) {
key, index, err := getKeyAndIndex(args)
if err != nil {
return "", err
}
return getValueByKeyAtIndex(req.Header, key, index)
},
VarResponseHeader: func(args []string, w *ResponseModifier, req *http.Request) (string, error) {
VarResponseHeader: func(args []string, w *httputils.ResponseModifier, req *http.Request) (string, error) {
key, index, err := getKeyAndIndex(args)
if err != nil {
return "", err
}
return getValueByKeyAtIndex(w.Header(), key, index)
},
VarQuery: func(args []string, w *ResponseModifier, req *http.Request) (string, error) {
VarQuery: func(args []string, w *httputils.ResponseModifier, req *http.Request) (string, error) {
key, index, err := getKeyAndIndex(args)
if err != nil {
return "", err
}
return getValueByKeyAtIndex(GetSharedData(w).GetQueries(req), key, index)
return getValueByKeyAtIndex(httputils.GetSharedData(w).GetQueries(req), key, index)
},
VarForm: func(args []string, w *ResponseModifier, req *http.Request) (string, error) {
VarForm: func(args []string, w *httputils.ResponseModifier, req *http.Request) (string, error) {
key, index, err := getKeyAndIndex(args)
if err != nil {
return "", err
@@ -50,7 +52,7 @@ var dynamicVarSubsMap = map[string]dynamicVarGetter{
}
return getValueByKeyAtIndex(req.Form, key, index)
},
VarPostForm: func(args []string, w *ResponseModifier, req *http.Request) (string, error) {
VarPostForm: func(args []string, w *httputils.ResponseModifier, req *http.Request) (string, error) {
key, index, err := getKeyAndIndex(args)
if err != nil {
return "", err

View File

@@ -7,6 +7,7 @@ import (
"strings"
"github.com/yusing/godoxy/internal/route/routes"
httputils "github.com/yusing/goutils/http"
)
const (
@@ -87,9 +88,9 @@ var staticReqVarSubsMap = map[string]reqVarGetter{
}
var staticRespVarSubsMap = map[string]respVarGetter{
VarRespContentType: func(resp *ResponseModifier) string { return resp.Header().Get("Content-Type") },
VarRespContentLen: func(resp *ResponseModifier) string { return strconv.Itoa(resp.ContentLength()) },
VarRespStatusCode: func(resp *ResponseModifier) string { return strconv.Itoa(resp.StatusCode()) },
VarRespContentType: func(resp *httputils.ResponseModifier) string { return resp.Header().Get("Content-Type") },
VarRespContentLen: func(resp *httputils.ResponseModifier) string { return resp.ContentLengthStr() },
VarRespStatusCode: func(resp *httputils.ResponseModifier) string { return strconv.Itoa(resp.StatusCode()) },
}
func stripFragment(s string) string {

View File

@@ -10,6 +10,7 @@ import (
"testing"
"github.com/stretchr/testify/require"
httputils "github.com/yusing/goutils/http"
)
func TestExtractArgs(t *testing.T) {
@@ -214,7 +215,7 @@ func TestExpandVars(t *testing.T) {
testRequest.PostForm = postFormData
// Create response modifier with headers
testResponseModifier := NewResponseModifier(httptest.NewRecorder())
testResponseModifier := httputils.NewResponseModifier(httptest.NewRecorder())
testResponseModifier.Header().Set("Content-Type", "text/html")
testResponseModifier.Header().Set("X-Custom-Resp", "resp-value")
testResponseModifier.WriteHeader(200)
@@ -251,7 +252,7 @@ func TestExpandVars(t *testing.T) {
{
name: "req_uri",
input: "$req_uri",
want: "/api/users?param1=value1&param2=value2#fragment",
want: "/api/users?param1=value1&param2=value2",
},
{
name: "req_host",
@@ -483,7 +484,7 @@ func TestExpandVars(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var out strings.Builder
err := ExpandVars(testResponseModifier, testRequest, tt.input, &out)
err := ExpandVars(httputils.NewResponseModifier(httptest.NewRecorder()), testRequest, tt.input, &out)
if tt.wantErr {
require.Error(t, err)
@@ -501,11 +502,11 @@ func TestExpandVars_Integration(t *testing.T) {
testRequest.Header.Set("User-Agent", "curl/7.68.0")
testRequest.RemoteAddr = "10.0.0.1:54321"
testResponseModifier := NewResponseModifier(httptest.NewRecorder())
testResponseModifier := httputils.NewResponseModifier(httptest.NewRecorder())
testResponseModifier.WriteHeader(200)
var out strings.Builder
err := ExpandVars(testResponseModifier, testRequest,
err := ExpandVars(httputils.NewResponseModifier(httptest.NewRecorder()), testRequest,
"$req_method $req_url $status_code User-Agent=$header(User-Agent)",
&out)
@@ -516,7 +517,7 @@ func TestExpandVars_Integration(t *testing.T) {
t.Run("with query parameters", func(t *testing.T) {
testRequest := httptest.NewRequest("GET", "http://example.com/search?q=test&page=1", nil)
testResponseModifier := NewResponseModifier(httptest.NewRecorder())
testResponseModifier := httputils.NewResponseModifier(httptest.NewRecorder())
var out strings.Builder
err := ExpandVars(testResponseModifier, testRequest,
@@ -530,13 +531,13 @@ func TestExpandVars_Integration(t *testing.T) {
t.Run("response headers", func(t *testing.T) {
testRequest := httptest.NewRequest("GET", "/", nil)
testResponseModifier := NewResponseModifier(httptest.NewRecorder())
testResponseModifier := httputils.NewResponseModifier(httptest.NewRecorder())
testResponseModifier.Header().Set("Cache-Control", "no-cache")
testResponseModifier.Header().Set("X-Rate-Limit", "100")
testResponseModifier.WriteHeader(200)
var out strings.Builder
err := ExpandVars(testResponseModifier, testRequest,
err := ExpandVars(httputils.NewResponseModifier(httptest.NewRecorder()), testRequest,
"Status: $status_code, Cache: $resp_header(Cache-Control), Limit: $resp_header(X-Rate-Limit)",
&out)
@@ -569,7 +570,7 @@ func TestExpandVars_RequestSchemes(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testResponseModifier := NewResponseModifier(httptest.NewRecorder())
testResponseModifier := httputils.NewResponseModifier(httptest.NewRecorder())
var out strings.Builder
err := ExpandVars(testResponseModifier, tt.request, "$req_scheme", &out)
require.NoError(t, err)
@@ -582,7 +583,7 @@ func TestExpandVars_UpstreamVariables(t *testing.T) {
// Upstream variables require context from routes package
testRequest := httptest.NewRequest("GET", "/", nil)
testResponseModifier := NewResponseModifier(httptest.NewRecorder())
testResponseModifier := httputils.NewResponseModifier(httptest.NewRecorder())
// Test that upstream variables don't cause errors even when not set
upstreamVars := []string{
@@ -609,7 +610,7 @@ func TestExpandVars_NoHostPort(t *testing.T) {
testRequest := httptest.NewRequest("GET", "/", nil)
testRequest.Host = "example.com" // No port
testResponseModifier := NewResponseModifier(httptest.NewRecorder())
testResponseModifier := httputils.NewResponseModifier(httptest.NewRecorder())
t.Run("req_host without port", func(t *testing.T) {
var out strings.Builder
@@ -631,7 +632,7 @@ func TestExpandVars_NoRemotePort(t *testing.T) {
testRequest := httptest.NewRequest("GET", "/", nil)
testRequest.RemoteAddr = "192.168.1.1" // No port
testResponseModifier := NewResponseModifier(httptest.NewRecorder())
testResponseModifier := httputils.NewResponseModifier(httptest.NewRecorder())
t.Run("remote_host without port", func(t *testing.T) {
var out strings.Builder
@@ -650,7 +651,7 @@ func TestExpandVars_NoRemotePort(t *testing.T) {
func TestExpandVars_WhitespaceHandling(t *testing.T) {
testRequest := httptest.NewRequest("GET", "/test", nil)
testResponseModifier := NewResponseModifier(httptest.NewRecorder())
testResponseModifier := httputils.NewResponseModifier(httptest.NewRecorder())
var out strings.Builder
err := ExpandVars(testResponseModifier, testRequest, "$req_method $req_path", &out)

View File

@@ -69,10 +69,6 @@ func (r *StreamRoute) Start(parent task.Parent) gperr.Error {
}
}
if r.ShouldExclude() {
return nil
}
r.ListenAndServe(r.task.Context(), nil, nil)
r.l = r.l.With().Stringer("rurl", r.ProxyURL).Stringer("laddr", r.LocalAddr()).Logger()
r.l.Info().Msg("stream started")

View File

@@ -13,7 +13,6 @@ import (
"github.com/go-playground/validator/v10"
"github.com/goccy/go-yaml"
"github.com/puzpuzpuz/xsync/v4"
"github.com/yusing/godoxy/internal/utils"
gi "github.com/yusing/gointernals"
"github.com/yusing/goutils/env"
gperr "github.com/yusing/goutils/errs"
@@ -22,6 +21,22 @@ import (
type SerializedObject = map[string]any
// ToSerializedObject converts a map[string]VT to a SerializedObject.
func ToSerializedObject[VT any](m map[string]VT) SerializedObject {
so := make(SerializedObject, len(m))
for k, v := range m {
so[k] = v
}
return so
}
func init() {
strutils.SetJSONMarshaler(sonic.Marshal)
strutils.SetJSONUnmarshaler(sonic.Unmarshal)
strutils.SetYAMLMarshaler(yaml.Marshal)
strutils.SetYAMLUnmarshaler(yaml.Unmarshal)
}
type MapUnmarshaller interface {
UnmarshalMap(m map[string]any) gperr.Error
}
@@ -300,7 +315,7 @@ func mapUnmarshalValidate(src SerializedObject, dstV reflect.Value, checkValidat
errs.Add(err.Subject(k))
}
} else {
errs.Add(ErrUnknownField.Subject(k).With(gperr.DoYouMean(utils.NearestField(k, info.fieldNames))))
errs.Add(ErrUnknownField.Subject(k).With(gperr.DoYouMeanField(k, info.fieldNames)))
}
}
if info.hasValidateTag && checkValidateTag {

View File

@@ -5,7 +5,6 @@ import (
"github.com/moby/moby/api/types/container"
"github.com/yusing/ds/ordered"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
)
@@ -14,8 +13,6 @@ type (
PortMapping = map[int]container.PortSummary
Container struct {
_ utils.NoCopy
DockerHost string `json:"docker_host"`
Image *ContainerImage `json:"image"`
ContainerName string `json:"container_name"`

View File

@@ -3,25 +3,39 @@ package types
import (
"context"
"time"
"github.com/yusing/godoxy/internal/common"
)
type HealthCheckConfig struct {
Disable bool `json:"disable,omitempty" aliases:"disabled"`
Path string `json:"path,omitempty" validate:"omitempty,uri,startswith=/"`
UseGet bool `json:"use_get,omitempty"`
Path string `json:"path,omitempty" validate:"omitempty,uri,startswith=/"`
Interval time.Duration `json:"interval" validate:"omitempty,min=1s" swaggertype:"primitive,integer"`
Timeout time.Duration `json:"timeout" validate:"omitempty,min=1s" swaggertype:"primitive,integer"`
Retries int64 `json:"retries"` // <0: immediate, >=0: threshold
Retries int64 `json:"retries"` // <0: immediate, 0: default, >0: threshold
BaseContext func() context.Context `json:"-"`
} // @name HealthCheckConfig
func DefaultHealthConfig() *HealthCheckConfig {
return &HealthCheckConfig{
Interval: common.HealthCheckIntervalDefault,
Timeout: common.HealthCheckTimeoutDefault,
Retries: int64(common.HealthCheckDownNotifyDelayDefault / common.HealthCheckIntervalDefault),
const (
HealthCheckIntervalDefault = 5 * time.Second
HealthCheckTimeoutDefault = 5 * time.Second
HealthCheckDownNotifyDelayDefault = 15 * time.Second
)
func (hc *HealthCheckConfig) ApplyDefaults(defaults HealthCheckConfig) {
if hc.Interval == 0 {
hc.Interval = defaults.Interval
if hc.Interval == 0 {
hc.Interval = HealthCheckIntervalDefault
}
}
if hc.Timeout == 0 {
hc.Timeout = defaults.Timeout
if hc.Timeout == 0 {
hc.Timeout = HealthCheckTimeoutDefault
}
}
if hc.Retries == 0 {
hc.Retries = int64(HealthCheckDownNotifyDelayDefault / hc.Interval)
}
}

View File

@@ -7,9 +7,9 @@ import (
"github.com/yusing/godoxy/internal/homepage"
nettypes "github.com/yusing/godoxy/internal/net/types"
provider "github.com/yusing/godoxy/internal/route/provider/types"
"github.com/yusing/godoxy/internal/utils/pool"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/http/reverseproxy"
"github.com/yusing/goutils/pool"
"github.com/yusing/goutils/task"
)
@@ -29,7 +29,7 @@ type (
Started() <-chan struct{}
IdlewatcherConfig() *IdlewatcherConfig
HealthCheckConfig() *HealthCheckConfig
HealthCheckConfig() HealthCheckConfig
LoadBalanceConfig() *LoadBalancerConfig
HomepageItem() homepage.Item
DisplayName() string
@@ -52,6 +52,10 @@ type (
HTTPRoute
ReverseProxy() *reverseproxy.ReverseProxy
}
FileServerRoute interface {
HTTPRoute
RootPath() string
}
StreamRoute interface {
Route
nettypes.Stream

View File

@@ -1,36 +0,0 @@
package utils
import (
"fmt"
"os"
"path"
)
// Recursively lists all files in a directory until `maxDepth` is reached
// Returns a slice of file paths relative to `dir`.
func ListFiles(dir string, maxDepth int, hideHidden ...bool) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("error listing directory %s: %w", dir, err)
}
hideHiddenFiles := len(hideHidden) > 0 && hideHidden[0]
files := make([]string, 0)
for _, entry := range entries {
if hideHiddenFiles && entry.Name()[0] == '.' {
continue
}
if entry.IsDir() {
if maxDepth <= 0 {
continue
}
subEntries, err := ListFiles(path.Join(dir, entry.Name()), maxDepth-1)
if err != nil {
return nil, err
}
files = append(files, subEntries...)
} else {
files = append(files, path.Join(dir, entry.Name()))
}
}
return files, nil
}

View File

@@ -1,50 +0,0 @@
package utils
import (
"reflect"
strutils "github.com/yusing/goutils/strings"
)
func NearestField(input string, s any) string {
minDistance := -1
nearestField := ""
var fields []string
switch s := s.(type) {
case []string:
fields = s
default:
t := reflect.TypeOf(s)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
switch t.Kind() {
case reflect.Struct:
fields = make([]string, 0)
for i := range t.NumField() {
jsonTag, ok := t.Field(i).Tag.Lookup("json")
if ok {
fields = append(fields, jsonTag)
} else {
fields = append(fields, t.Field(i).Name)
}
}
case reflect.Map:
keys := reflect.ValueOf(s).MapKeys()
fields = make([]string, len(keys))
for i, key := range keys {
fields[i] = key.String()
}
default:
panic("NearestField unsupported type: " + t.String())
}
}
for _, field := range fields {
distance := strutils.LevenshteinDistance(input, field)
if minDistance == -1 || distance < minDistance {
minDistance = distance
nearestField = field
}
}
return nearestField
}

View File

@@ -1,123 +0,0 @@
package pool
import (
"sort"
"sync/atomic"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
)
type (
Pool[T Object] struct {
m *xsync.Map[string, T]
name string
disableLog atomic.Bool
}
// Preferable allows an object to express deterministic replacement preference
// when multiple objects with the same key are added to the pool.
// If new.PreferOver(old) returns true, the new object replaces the old one.
Preferable interface {
PreferOver(other any) bool
}
Object interface {
Key() string
Name() string
}
ObjectWithDisplayName interface {
Object
DisplayName() string
}
)
func New[T Object](name string) Pool[T] {
return Pool[T]{m: xsync.NewMap[string, T](), name: name}
}
func (p *Pool[T]) ToggleLog(v bool) {
p.disableLog.Store(v)
}
func (p *Pool[T]) Name() string {
return p.name
}
func (p *Pool[T]) Add(obj T) {
p.AddKey(obj.Key(), obj)
}
func (p *Pool[T]) AddKey(key string, obj T) {
if cur, exists := p.m.Load(key); exists {
if newPref, ok := any(obj).(Preferable); ok {
if !newPref.PreferOver(cur) {
// keep existing
return
}
}
}
p.checkExists(key)
p.m.Store(key, obj)
p.logAction("added", obj)
}
func (p *Pool[T]) AddIfNotExists(obj T) (actual T, added bool) {
actual, loaded := p.m.LoadOrStore(obj.Key(), obj)
if !loaded {
p.logAction("added", obj)
}
return actual, !loaded
}
func (p *Pool[T]) Del(obj T) {
p.m.Delete(obj.Key())
p.logAction("removed", obj)
}
func (p *Pool[T]) DelKey(key string) {
if v, exists := p.m.LoadAndDelete(key); exists {
p.logAction("removed", v)
}
}
func (p *Pool[T]) Get(key string) (T, bool) {
return p.m.Load(key)
}
func (p *Pool[T]) Size() int {
return p.m.Size()
}
func (p *Pool[T]) Clear() {
p.m.Clear()
}
func (p *Pool[T]) Iter(fn func(k string, v T) bool) {
p.m.Range(fn)
}
func (p *Pool[T]) Slice() []T {
slice := make([]T, 0, p.m.Size())
for _, v := range p.m.Range {
slice = append(slice, v)
}
sort.Slice(slice, func(i, j int) bool {
return slice[i].Name() < slice[j].Name()
})
return slice
}
func (p *Pool[T]) logAction(action string, obj T) {
if p.disableLog.Load() {
return
}
if withName, ok := any(obj).(ObjectWithDisplayName); ok {
disp, name := withName.DisplayName(), withName.Name()
if disp != name {
log.Info().Msgf("%s: %s %s (%s)", p.name, action, disp, name)
} else {
log.Info().Msgf("%s: %s %s", p.name, action, name)
}
} else {
log.Info().Msgf("%s: %s %s", p.name, action, obj.Name())
}
}

View File

@@ -1,15 +0,0 @@
//go:build debug
package pool
import (
"runtime/debug"
"github.com/rs/zerolog/log"
)
func (p Pool[T]) checkExists(key string) {
if _, ok := p.m.Load(key); ok {
log.Warn().Msgf("%s: key %s already exists\nstacktrace: %s", p.name, key, string(debug.Stack()))
}
}

View File

@@ -1,7 +0,0 @@
//go:build !debug
package pool
func (p Pool[T]) checkExists(key string) {
// no-op in production
}

View File

@@ -1,44 +0,0 @@
package utils
import (
"time"
"go.uber.org/atomic"
)
var (
TimeNow = DefaultTimeNow
shouldCallTimeNow atomic.Bool
timeNowTicker = time.NewTicker(shouldCallTimeNowInterval)
lastTimeNow = atomic.NewTime(time.Now())
)
const shouldCallTimeNowInterval = 100 * time.Millisecond
func MockTimeNow(t time.Time) {
TimeNow = func() time.Time {
return t
}
}
// DefaultTimeNow is a time.Now wrapper that reduces the number of calls to time.Now
// by caching the result and only allow calling time.Now when the ticker fires.
//
// Returned value may have +-100ms error.
func DefaultTimeNow() time.Time {
swapped := shouldCallTimeNow.CompareAndSwap(false, true)
if swapped { // first call
now := time.Now()
lastTimeNow.Store(now)
return now
}
return lastTimeNow.Load()
}
func init() {
go func() {
for range timeNowTicker.C {
shouldCallTimeNow.Store(true)
}
}()
}

View File

@@ -1,104 +0,0 @@
package utils
import (
"testing"
"time"
)
var sink time.Time
func BenchmarkTimeNow(b *testing.B) {
b.Run("default", func(b *testing.B) {
for b.Loop() {
sink = time.Now()
}
})
b.Run("reduced_call", func(b *testing.B) {
for b.Loop() {
sink = DefaultTimeNow()
}
})
}
func TestDefaultTimeNow(t *testing.T) {
// Get initial time
t1 := DefaultTimeNow()
// Second call should return the same time without calling time.Now
t2 := DefaultTimeNow()
if !t1.Equal(t2) {
t.Errorf("Expected t1 == t2, got t1 = %v, t2 = %v", t1, t2)
}
// Set shouldCallTimeNow to true
shouldCallTimeNow.Store(true)
// This should update the lastTimeNow
t3 := DefaultTimeNow()
// The time should have changed
if t2.Equal(t3) {
t.Errorf("Expected t2 != t3, got t2 = %v, t3 = %v", t2, t3)
}
// Fourth call should return the same time as third call
t4 := DefaultTimeNow()
if !t3.Equal(t4) {
t.Errorf("Expected t3 == t4, got t3 = %v, t4 = %v", t3, t4)
}
}
func TestMockTimeNow(t *testing.T) {
// Save the original TimeNow function to restore later
originalTimeNow := TimeNow
defer func() {
TimeNow = originalTimeNow
}()
// Create a fixed time
fixedTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
// Mock the time
MockTimeNow(fixedTime)
// TimeNow should return the fixed time
result := TimeNow()
if !result.Equal(fixedTime) {
t.Errorf("Expected %v, got %v", fixedTime, result)
}
}
func TestTimeNowTicker(t *testing.T) {
// This test verifies that the ticker properly updates shouldCallTimeNow
// Reset the flag
shouldCallTimeNow.Store(false)
// Wait for the ticker to tick (slightly more than the interval)
time.Sleep(shouldCallTimeNowInterval + 10*time.Millisecond)
// The ticker should have set shouldCallTimeNow to true
if !shouldCallTimeNow.Load() {
t.Error("Expected shouldCallTimeNow to be true after ticker interval")
}
// Call DefaultTimeNow which should reset the flag
DefaultTimeNow()
// Check that the flag is reset
if shouldCallTimeNow.Load() {
t.Error("Expected shouldCallTimeNow to be false after calling DefaultTimeNow")
}
}
/*
BenchmarkTimeNow
BenchmarkTimeNow/default
BenchmarkTimeNow/default-20 48158628 24.86 ns/op 0 B/op 0 allocs/op
BenchmarkTimeNow/reduced_call
BenchmarkTimeNow/reduced_call-20 1000000000 1.000 ns/op 0 B/op 0 allocs/op
*/

View File

@@ -45,7 +45,7 @@ func (target *AgentCheckHealthTarget) displayURL() *url.URL {
}
}
func NewAgentProxiedMonitor(agent *agentPkg.AgentConfig, config *types.HealthCheckConfig, target *AgentCheckHealthTarget) *AgentProxiedMonitor {
func NewAgentProxiedMonitor(agent *agentPkg.AgentConfig, config types.HealthCheckConfig, target *AgentCheckHealthTarget) *AgentProxiedMonitor {
mon := &AgentProxiedMonitor{
agent: agent,
}

View File

@@ -26,7 +26,7 @@ type DockerHealthMonitor struct {
const dockerFailuresThreshold = 3
func NewDockerHealthMonitor(client *docker.SharedClient, containerID, alias string, config *types.HealthCheckConfig, fallback types.HealthChecker) *DockerHealthMonitor {
func NewDockerHealthMonitor(client *docker.SharedClient, containerID, alias string, config types.HealthCheckConfig, fallback types.HealthChecker) *DockerHealthMonitor {
mon := new(DockerHealthMonitor)
mon.client = client
mon.containerID = containerID

View File

@@ -12,7 +12,7 @@ type FileServerHealthMonitor struct {
path string
}
func NewFileServerHealthMonitor(config *types.HealthCheckConfig, path string) *FileServerHealthMonitor {
func NewFileServerHealthMonitor(config types.HealthCheckConfig, path string) *FileServerHealthMonitor {
mon := &FileServerHealthMonitor{path: path}
mon.monitor = newMonitor(nil, config, mon.CheckHealth)
return mon

View File

@@ -29,7 +29,7 @@ var pinger = &fasthttp.Client{
NoDefaultUserAgentHeader: true,
}
func NewHTTPHealthMonitor(url *url.URL, config *types.HealthCheckConfig) *HTTPHealthMonitor {
func NewHTTPHealthMonitor(url *url.URL, config types.HealthCheckConfig) *HTTPHealthMonitor {
mon := new(HTTPHealthMonitor)
mon.monitor = newMonitor(url, config, mon.CheckHealth)
if config.UseGet {

View File

@@ -10,7 +10,7 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/types"
@@ -24,7 +24,7 @@ type (
HealthCheckFunc func() (result types.HealthCheckResult, err error)
monitor struct {
service string
config *types.HealthCheckConfig
config types.HealthCheckConfig
url synk.Value[*url.URL]
status synk.Value[types.HealthStatus]
@@ -51,8 +51,10 @@ func NewMonitor(r types.Route) types.HealthMonCheck {
mon = NewAgentProxiedMonitor(r.GetAgent(), r.HealthCheckConfig(), AgentTargetFromURL(&r.TargetURL().URL))
} else {
switch r := r.(type) {
case types.HTTPRoute:
case types.ReverseProxyRoute:
mon = NewHTTPHealthMonitor(&r.TargetURL().URL, r.HealthCheckConfig())
case types.FileServerRoute:
mon = NewFileServerHealthMonitor(r.HealthCheckConfig(), r.RootPath())
case types.StreamRoute:
mon = NewRawHealthMonitor(&r.TargetURL().URL, r.HealthCheckConfig())
default:
@@ -71,15 +73,10 @@ func NewMonitor(r types.Route) types.HealthMonCheck {
return mon
}
func newMonitor(u *url.URL, config *types.HealthCheckConfig, healthCheckFunc HealthCheckFunc) *monitor {
if config.Retries == 0 {
if config.Interval == 0 {
config.Interval = common.HealthCheckIntervalDefault
}
config.Retries = int64(common.HealthCheckDownNotifyDelayDefault / config.Interval)
}
func newMonitor(u *url.URL, cfg types.HealthCheckConfig, healthCheckFunc HealthCheckFunc) *monitor {
cfg.ApplyDefaults(config.DefaultConfig().Defaults.HealthCheck)
mon := &monitor{
config: config,
config: cfg,
checkHealth: healthCheckFunc,
startTime: time.Now(),
notifyFunc: notif.Notify,
@@ -200,7 +197,7 @@ func (mon *monitor) URL() *url.URL {
// Config implements HealthChecker.
func (mon *monitor) Config() *types.HealthCheckConfig {
return mon.config
return &mon.config
}
// Status implements HealthMonitor.
@@ -241,7 +238,7 @@ func (mon *monitor) MarshalJSON() ([]byte, error) {
res := mon.lastResult.Load()
return (&types.HealthJSONRepr{
Name: mon.service,
Config: mon.config,
Config: &mon.config,
Status: mon.status.Load(),
Started: mon.startTime,
Uptime: mon.Uptime(),

View File

@@ -28,7 +28,7 @@ func (t *testNotificationTracker) getStats() (up, down int, last string) {
}
// Create test monitor with mock health checker - returns both monitor and tracker
func createTestMonitor(config *types.HealthCheckConfig, checkFunc HealthCheckFunc) (*monitor, *testNotificationTracker) {
func createTestMonitor(config types.HealthCheckConfig, checkFunc HealthCheckFunc) (*monitor, *testNotificationTracker) {
testURL, _ := url.Parse("http://localhost:8080")
mon := newMonitor(testURL, config, checkFunc)
@@ -56,7 +56,7 @@ func createTestMonitor(config *types.HealthCheckConfig, checkFunc HealthCheckFun
}
func TestNotification_ImmediateNotifyAfterZero(t *testing.T) {
config := &types.HealthCheckConfig{
config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond,
Retries: -1, // Immediate notification
@@ -91,7 +91,7 @@ func TestNotification_ImmediateNotifyAfterZero(t *testing.T) {
}
func TestNotification_WithNotifyAfterThreshold(t *testing.T) {
config := &types.HealthCheckConfig{
config := types.HealthCheckConfig{
Interval: 50 * time.Millisecond,
Timeout: 50 * time.Millisecond,
Retries: 2, // Notify after 2 consecutive failures
@@ -130,7 +130,7 @@ func TestNotification_WithNotifyAfterThreshold(t *testing.T) {
}
func TestNotification_ServiceRecoversBeforeThreshold(t *testing.T) {
config := &types.HealthCheckConfig{
config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond,
Retries: 3, // Notify after 3 consecutive failures
@@ -179,7 +179,7 @@ func TestNotification_ServiceRecoversBeforeThreshold(t *testing.T) {
}
func TestNotification_ConsecutiveFailureReset(t *testing.T) {
config := &types.HealthCheckConfig{
config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond,
Retries: 2, // Notify after 2 consecutive failures
@@ -240,7 +240,7 @@ func TestNotification_ConsecutiveFailureReset(t *testing.T) {
}
func TestNotification_ContextCancellation(t *testing.T) {
config := &types.HealthCheckConfig{
config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond,
Retries: 1,
@@ -279,7 +279,7 @@ func TestNotification_ContextCancellation(t *testing.T) {
}
func TestImmediateUpNotification(t *testing.T) {
config := &types.HealthCheckConfig{
config := types.HealthCheckConfig{
Interval: 100 * time.Millisecond,
Timeout: 50 * time.Millisecond,
Retries: 2, // NotifyAfter should not affect up notifications

View File

@@ -17,7 +17,7 @@ type (
}
)
func NewRawHealthMonitor(url *url.URL, config *types.HealthCheckConfig) *RawHealthMonitor {
func NewRawHealthMonitor(url *url.URL, config types.HealthCheckConfig) *RawHealthMonitor {
mon := new(RawHealthMonitor)
mon.monitor = newMonitor(url, config, mon.CheckHealth)
mon.dialer = &net.Dialer{

View File

@@ -1,6 +1,6 @@
module github.com/yusing/godoxy/socketproxy
go 1.25.4
go 1.25.5
exclude github.com/yusing/goutils v0.4.2