Compare commits

..

11 Commits

Author SHA1 Message Date
yusing
bd40c46928 chore(deps): update plugin and linter versions in trunk.yaml
- Updated trunk plugin reference from v1.7.2 to v1.7.4
- Upgraded Go runtime from 1.24.3 to 1.25.6
- Updated linting tools: checkov to 3.2.500, golangci-lint2 to 2.8.0, actionlint to 1.7.10, osv-scanner to 2.3.2, oxipng to 10.1.0, and prettier to 3.8.1
- Updated trufflehog to 3.93.1
- Added ignore rule for all linters in internal/api/v1/docs/**
2026-02-08 12:08:01 +08:00
yusing
6da7227f9b refactor(errs): migrate from gperr.Error to standard Go error interface
This is a large-scale refactoring across the codebase that replaces the custom
`gperr.Error` type with Go's standard `error` interface. The changes include:

- Replacing `gperr.Error` return types with `error` in function signatures
- Using `errors.New()` and `fmt.Errorf()` instead of `gperr.New()` and `gperr.Errorf()`
- Using `%w` format verb for error wrapping instead of `.With()` method
- Replacing `gperr.Subject()` calls with `gperr.PrependSubject()`
- Converting error logging from `gperr.Log*()` functions to zerolog's `.Err().Msg()` pattern
- Update NewLogger to handle multiline error message
- Updating `goutils` submodule to latest commit

This refactoring aligns with Go idioms and removes the dependency on
custom error handling abstractions in favor of standard library patterns.
2026-02-08 12:07:36 +08:00
yusing
7eb2a78041 test: complete available fields for testing 2026-02-08 10:08:12 +08:00
yusing
e227b9e06f chore(deps): upgrade dependencies 2026-02-08 09:33:44 +08:00
yusing
5c8126c2e6 refactor(route/logging): streamline log messages with EmbedObject for improved clarity
Updated logging statements across multiple files to utilize EmbedObject for enhanced context in log messages. This change improves the readability and consistency of log outputs, particularly in health monitoring and route validation processes.
2026-02-08 09:20:45 +08:00
Yuzerion
31b4fedf72 refactor(entrypoint): move route registry into entrypoint context (#200)
- Introduced `NewTestRoute` function to simplify route creation in benchmark tests.
- Replaced direct route validation and starting with error handling using `require.NoError`.
- Updated server retrieval to use `common.ProxyHTTPAddr` for consistency.
- Improved logging for HTTP route addition errors in `AddRoute` method.

* fix(tcp): wrap proxy proto listener before acl

* refactor(entrypoint): propagate errors from route registration and stream serving

* fix(docs): correct swagger and package README
2026-02-08 09:17:46 +08:00
yusing
bd49f1b348 chore: upgrade go version to 1.25.7 2026-02-06 00:01:22 +08:00
yusing
953ec80556 BREAKING(api): remove /reload api 2026-02-05 22:56:43 +08:00
yusing
fc540ea419 fix(config): handle critical config errors
Propagate critical init and entrypoint failures to halt startup
and log them as fatal during config loading
2026-02-05 22:56:09 +08:00
yusing
211e4ad465 refactor: update webui rules and docker compose
- Docker compose
  - tmpfs update /app/.next/cache to /app/node_modules/.cache
  - tmpfs add /tmp
- Rules
  - Update rules for tanstack start + nitro
  - Stricter webui rules
  - Add webui dev rules
2026-02-05 22:53:35 +08:00
yusing
0a2df3b9e3 refactor(entrypoint): rename shortLinkTree to shortLinkMatcher 2026-02-01 10:00:04 +08:00
186 changed files with 2739 additions and 2075 deletions

View File

@@ -7,32 +7,37 @@ cli:
plugins:
sources:
- id: trunk
ref: v1.7.2
ref: v1.7.4
uri: https://github.com/trunk-io/plugins
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
runtimes:
enabled:
- node@22.16.0
- python@3.10.8
- go@1.24.3
- go@1.25.6
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
lint:
disabled:
- markdownlint
- yamllint
enabled:
- checkov@3.2.471
- golangci-lint2@2.5.0
- checkov@3.2.500
- golangci-lint2@2.8.0
- hadolint@2.14.0
- actionlint@1.7.7
- actionlint@1.7.10
- git-diff-check
- gofmt@1.20.4
- osv-scanner@2.2.2
- oxipng@9.1.5
- prettier@3.6.2
- osv-scanner@2.3.2
- oxipng@10.1.0
- prettier@3.8.1
- shellcheck@0.11.0
- shfmt@3.6.0
- trufflehog@3.90.8
- trufflehog@3.93.1
ignore:
- linters: [ALL]
paths:
- internal/api/v1/docs/**
actions:
disabled:
- trunk-announce

View File

@@ -1,5 +1,5 @@
# Stage 1: deps
FROM golang:1.25.6-alpine AS deps
FROM golang:1.25.7-alpine AS deps
HEALTHCHECK NONE
# package version does not matter

View File

@@ -19,7 +19,6 @@ import (
"github.com/yusing/godoxy/agent/pkg/handler"
"github.com/yusing/godoxy/internal/metrics/systeminfo"
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
"github.com/yusing/goutils/version"
@@ -72,7 +71,7 @@ Tips:
// - Otherwise: route to HTTPS API handler
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: env.AgentPort})
if err != nil {
gperr.LogFatal("failed to listen on port", err)
log.Fatal().Err(err).Msg("failed to listen on port")
}
caCertPool := x509.NewCertPool()
@@ -148,7 +147,7 @@ Tips:
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
l, err := net.Listen("tcp", socketproxy.ListenAddr)
if err != nil {
gperr.LogFatal("failed to listen on port", err)
log.Fatal().Err(err).Msg("failed to listen on port")
}
errLog := log.Logger.With().Str("level", "error").Str("component", "socketproxy").Logger()
srv := http.Server{
@@ -158,10 +157,15 @@ Tips:
},
ErrorLog: stdlog.New(&errLog, "", 0),
}
srv.Serve(l)
go func() {
err := srv.Serve(l)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Error().Err(err).Msg("socket proxy server stopped with error")
}
}()
}
systeminfo.Poller.Start()
systeminfo.Poller.Start(t)
task.WaitExit(3)
}

View File

@@ -1,6 +1,6 @@
module github.com/yusing/godoxy/agent
go 1.25.6
go 1.25.7
exclude (
github.com/moby/moby/api v1.53.0 // allow older daemon versions
@@ -27,7 +27,7 @@ require (
github.com/pion/transport/v3 v3.1.1
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1
github.com/yusing/godoxy v0.25.2
github.com/yusing/godoxy v0.25.3
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
github.com/yusing/goutils v0.7.0
)
@@ -43,12 +43,12 @@ require (
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.2.0+incompatible // indirect
github.com/docker/cli v29.2.1+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -81,7 +81,7 @@ require (
github.com/puzpuzpuz/xsync/v4 v4.4.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
github.com/shirou/gopsutil/v4 v4.26.1 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
@@ -90,15 +90,15 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.69.0 // indirect
github.com/yusing/ds v0.4.1 // indirect
github.com/yusing/gointernals v0.1.16 // indirect
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468 // indirect
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468 // indirect
github.com/yusing/gointernals v0.1.18 // indirect
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260207122228-56663372deda // indirect
github.com/yusing/goutils/http/websocket v0.0.0-20260207122228-56663372deda // 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.64.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect

View File

@@ -37,8 +37,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/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.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -49,8 +49,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.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=
@@ -153,8 +153,8 @@ github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkY
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pires/go-proxyproto v0.10.0 h1:08wrdt9NQYTjLWeag3EBIS7ZNi6Vwl3rGsEjVLaAhvU=
github.com/pires/go-proxyproto v0.10.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
@@ -174,10 +174,10 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
@@ -210,24 +210,24 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
github.com/yusing/gointernals v0.1.18 h1:ou8/0tPURUgAOBJu3TN/iWF4S/5ZYQaap+rVkaJNUMw=
github.com/yusing/gointernals v0.1.18/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=

View File

@@ -216,7 +216,7 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
cfg.l = log.With().Str("agent", cfg.Name).Logger()
if err := streamUnsupportedErrs.Error(); err != nil {
gperr.LogWarn("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work", err, &cfg.l)
cfg.l.Warn().Err(err).Msg("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work")
}
if serverVersion.IsNewerThanMajor(cfg.Version) {

View File

@@ -1,4 +1,4 @@
FROM golang:1.25.6-alpine AS builder
FROM golang:1.25.7-alpine AS builder
HEALTHCHECK NONE

View File

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

View File

@@ -181,7 +181,6 @@ func newApiHandler(debugMux *debugMux) *gin.Engine {
registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon)
registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health)
registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons)
registerGinRoute(v1, "POST", "Config reload", "/reload", apiV1.Reload)
registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats)
route := v1.Group("/route")

View File

@@ -1,4 +1,4 @@
FROM golang:1.25.6-alpine AS builder
FROM golang:1.25.7-alpine AS builder
HEALTHCHECK NONE

View File

@@ -1,6 +1,6 @@
module github.com/yusing/godoxy/cmd/h2c_test_server
go 1.25.6
go 1.25.7
require golang.org/x/net v0.49.0

View File

@@ -1,12 +1,12 @@
package main
import (
"errors"
"os"
"sync"
"time"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/api"
"github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/config"
@@ -14,12 +14,8 @@ import (
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
"github.com/yusing/godoxy/internal/logging"
"github.com/yusing/godoxy/internal/logging/memlogger"
"github.com/yusing/godoxy/internal/metrics/systeminfo"
"github.com/yusing/godoxy/internal/metrics/uptime"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/route/rules"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/task"
"github.com/yusing/goutils/version"
)
@@ -51,7 +47,6 @@ func main() {
parallel(
dnsproviders.InitProviders,
iconlist.InitCache,
systeminfo.Poller.Start,
middleware.LoadComposeFiles,
)
@@ -66,35 +61,20 @@ func main() {
err := config.Load()
if err != nil {
gperr.LogWarn("errors in config", err)
var criticalErr config.CriticalError
if errors.As(err, &criticalErr) {
log.Fatal().Err(criticalErr).Msg("critical error in config")
}
log.Warn().Err(err).Msg("errors in config")
}
config.StartProxyServers()
if err := auth.Initialize(); err != nil {
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",
HTTPAddr: common.APIHTTPAddr,
Handler: api.NewHandler(true),
})
// Local API Handler is used for unauthenticated access.
if common.LocalAPIHTTPAddr != "" {
server.StartServer(task.RootTask("local_api_server", false), server.Options{
Name: "local_api",
HTTPAddr: common.LocalAPIHTTPAddr,
Handler: api.NewHandler(false),
})
}
listenDebugServer()
uptime.Poller.Start()
config.WatchChanges()
close(done)

View File

@@ -31,8 +31,8 @@ services:
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
read_only: true
tmpfs:
- /app/.next/cache # next image caching
- /tmp:rw
- /app/node_modules/.cache:rw
# for lite variant, do not change uid/gid
# - /var/cache/nginx:uid=101,gid=101
# - /run:uid=101,gid=101

48
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/yusing/godoxy
go 1.25.6
go 1.25.7
exclude (
github.com/moby/moby/api v1.53.0 // allow older daemon versions
@@ -30,7 +30,7 @@ require (
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
github.com/gotify/server/v2 v2.8.0 // reference the Message struct for json response
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
github.com/pires/go-proxyproto v0.9.2 // proxy protocol support
github.com/pires/go-proxyproto v0.10.0 // proxy protocol support
github.com/puzpuzpuz/xsync/v4 v4.4.0 // lock free map for concurrent operations
github.com/rs/zerolog v1.34.0 // logging
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
@@ -44,7 +44,7 @@ require (
require (
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
github.com/bytedance/sonic v1.15.0 // fast json parsing
github.com/docker/cli v29.2.0+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
github.com/docker/cli v29.2.1+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.3.1 // jwt authentication
github.com/luthermonson/go-proxmox v0.3.2 // proxmox API client
@@ -52,18 +52,18 @@ require (
github.com/moby/moby/client v0.2.1 // docker client
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
github.com/quic-go/quic-go v0.59.0 // http3 support
github.com/shirou/gopsutil/v4 v4.25.12 // system information
github.com/shirou/gopsutil/v4 v4.26.1 // system information
github.com/spf13/afero v1.15.0 // afero for file system operations
github.com/stretchr/testify v1.11.1 // testing framework
github.com/valyala/fasthttp v1.69.0 // fast http for health check
github.com/yusing/ds v0.4.1 // data structures and algorithms
github.com/yusing/godoxy/agent v0.0.0-20260129101716-0f13004ad6ba
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260129101716-0f13004ad6ba
github.com/yusing/gointernals v0.1.16
github.com/yusing/godoxy/agent v0.0.0-20260208011746-31b4fedf728b
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260208011746-31b4fedf728b
github.com/yusing/gointernals v0.1.18
github.com/yusing/goutils v0.7.0
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468
github.com/yusing/goutils/server v0.0.0-20260129081554-24e52ede7468
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260207122228-56663372deda
github.com/yusing/goutils/http/websocket v0.0.0-20260207122228-56663372deda
github.com/yusing/goutils/server v0.0.0-20260207122228-56663372deda
)
require (
@@ -89,7 +89,7 @@ require (
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.12 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -99,7 +99,7 @@ require (
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
@@ -124,25 +124,25 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/samber/slog-common v0.19.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
github.com/samber/slog-common v0.20.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.1 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/atomic v1.11.0
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/api v0.263.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/api v0.265.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.1 // indirect
@@ -172,11 +172,11 @@ require (
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/linode/linodego v1.64.0 // indirect
github.com/linode/linodego v1.65.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/nrdcg/goinwx v0.12.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.0 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pion/dtls/v3 v3.0.10 // indirect
github.com/pion/logging v0.2.4 // indirect
@@ -190,7 +190,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.26.1 // indirect
github.com/vultr/govultr/v3 v3.27.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/arch v0.23.0 // indirect
)

84
go.sum
View File

@@ -76,8 +76,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/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.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -94,8 +94,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.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=
@@ -153,8 +153,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
@@ -191,8 +191,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY=
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
@@ -227,10 +227,10 @@ github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 h1:eMzyN+jGJbxG4ut278uwIsUo9XacXc711lFjhKnaUso=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 h1:t34IpOa+8NfmjkU8bdWtYrLrmr346/FGhu8FlpJDQok=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0/go.mod h1:p95/OxVsdx71I2Qrck1GtIS87sRxcTRKXzUi5nWm9NY=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.0 h1:SgXDi/vitC+FA9jPl7T7i0d7kiC1JMFuS2FTlpg3B7o=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.0 h1:rOYbG56bYaW+skuCvzZyFoTPPXXUIOPhuQkilf6tkqo=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.0/go.mod h1:LTEIH1X6CBKyDtOT7CTTTgvvb2ANtGS7vLkEGt5zdog=
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=
@@ -251,8 +251,8 @@ github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pires/go-proxyproto v0.10.0 h1:08wrdt9NQYTjLWeag3EBIS7ZNi6Vwl3rGsEjVLaAhvU=
github.com/pires/go-proxyproto v0.10.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -278,10 +278,10 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc=
github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8=
github.com/samber/slog-zerolog/v2 v2.9.1 h1:RMOq8XqzfuGx1X0TEIlS9OXbbFmqLY2/wJppghz66YY=
github.com/samber/slog-zerolog/v2 v2.9.1/go.mod h1:DQYYve14WgCRN/XnKeHl4266jXK0DgYkYXkfZ4Fp98k=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
@@ -320,8 +320,8 @@ github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZy
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
github.com/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.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
@@ -329,26 +329,26 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
github.com/yusing/gointernals v0.1.18 h1:ou8/0tPURUgAOBJu3TN/iWF4S/5ZYQaap+rVkaJNUMw=
github.com/yusing/gointernals v0.1.18/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
@@ -447,14 +447,14 @@ golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk=
google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU=
google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

Submodule goutils updated: 52ea531e95...0f8a005f8a

View File

@@ -54,13 +54,13 @@ type Matchers []Matcher
### Exported functions and methods
```go
func (c *Config) Validate() gperr.Error
func (c *Config) Validate() error
```
Validates configuration and sets defaults. Must be called before `Start`.
```go
func (c *Config) Start(parent task.Parent) gperr.Error
func (c *Config) Start(parent task.Parent) error
```
Initializes the ACL, starts the logger and notification goroutines.
@@ -169,14 +169,14 @@ Configuration is loaded from `config/config.yml` under the `acl` key.
```yaml
acl:
default: "allow" # "allow" or "deny"
allow_local: true # Allow private/loopback IPs
default: "allow" # "allow" or "deny"
allow_local: true # Allow private/loopback IPs
log:
log_allowed: false # Log allowed connections
log_allowed: false # Log allowed connections
notify:
to: ["gotify"] # Notification providers
interval: "1m" # Notification interval
include_allowed: false # Include allowed in notifications
to: ["gotify"] # Notification providers
interval: "1m" # Notification interval
include_allowed: false # Include allowed in notifications
```
### Hot-reloading

View File

@@ -74,8 +74,6 @@ type ipLog struct {
allowed bool
}
type ContextKey struct{}
const cacheTTL = 1 * time.Minute
func (c *checkCache) Expired() bool {
@@ -89,7 +87,7 @@ const (
ACLDeny = "deny"
)
func (c *Config) Validate() gperr.Error {
func (c *Config) Validate() error {
switch c.Default {
case "", ACLAllow:
c.defaultAllow = true
@@ -133,7 +131,7 @@ func (c *Config) Valid() bool {
return c != nil && c.valErr == nil
}
func (c *Config) Start(parent task.Parent) gperr.Error {
func (c *Config) Start(parent task.Parent) error {
if c.Log != nil {
logger, err := accesslog.NewAccessLogger(parent, c.Log)
if err != nil {

View File

@@ -2,6 +2,7 @@ package acl
import (
"bytes"
"errors"
"net"
"strings"
@@ -38,9 +39,9 @@ var errMatcherFormat = gperr.Multiline().AddLines(
)
var (
errSyntax = gperr.New("syntax error")
errInvalidIP = gperr.New("invalid IP")
errInvalidCIDR = gperr.New("invalid CIDR")
errSyntax = errors.New("syntax error")
errInvalidIP = errors.New("invalid IP")
errInvalidCIDR = errors.New("invalid CIDR")
)
func (matcher *Matcher) Parse(s string) error {

View File

@@ -0,0 +1,9 @@
package acl
import "net"
type ACL interface {
IPAllowed(ip net.IP) bool
WrapTCP(l net.Listener) net.Listener
WrapUDP(l net.PacketConn) net.PacketConn
}

View File

@@ -0,0 +1,16 @@
package acl
import "context"
type ContextKey struct{}
func SetCtx(ctx interface{ SetValue(any, any) }, acl ACL) {
ctx.SetValue(ContextKey{}, acl)
}
func FromCtx(ctx context.Context) ACL {
if acl, ok := ctx.Value(ContextKey{}).(ACL); ok {
return acl
}
return nil
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/common"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
)
// @title GoDoxy API
@@ -76,7 +75,6 @@ func NewHandler(requireAuth bool) *gin.Engine {
v1.GET("/favicon", apiV1.FavIcon)
v1.GET("/health", apiV1.Health)
v1.GET("/icons", apiV1.Icons)
v1.POST("/reload", apiV1.Reload)
v1.GET("/stats", apiV1.Stats)
route := v1.Group("/route")
@@ -204,9 +202,8 @@ func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
logger := log.With().Str("uri", c.Request.RequestURI).Logger()
for _, err := range c.Errors {
gperr.LogError("Internal error", err.Err, &logger)
log.Err(err.Err).Str("uri", c.Request.RequestURI).Msg("Internal error")
}
if !c.IsWebsocket() {
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))

View File

@@ -2,6 +2,7 @@ package agentapi
import (
"context"
"errors"
"fmt"
"net/http"
"os"
@@ -13,7 +14,6 @@ import (
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/route/provider"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
)
type VerifyNewAgentRequest struct {
@@ -84,9 +84,9 @@ func Verify(c *gin.Context) {
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
}
var errAgentAlreadyExists = gperr.New("agent already exists")
var errAgentAlreadyExists = errors.New("agent already exists")
func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, error) {
var agentCfg agent.AgentConfig
agentCfg.Addr = host
agentCfg.Runtime = containerRuntime
@@ -105,12 +105,12 @@ func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client a
err := agentCfg.InitWithCerts(ctx, ca.Cert, client.Cert, client.Key)
if err != nil {
return 0, gperr.Wrap(err, "failed to initialize agent config")
return 0, fmt.Errorf("failed to initialize agent config: %w", err)
}
provider := provider.NewAgentProvider(&agentCfg)
if _, loaded := cfgState.LoadOrStoreProvider(provider.String(), provider); loaded {
return 0, gperr.Errorf("provider %s already exists", provider.String())
return 0, fmt.Errorf("provider %s already exists", provider.String())
}
// agent must be added before loading routes
@@ -122,7 +122,7 @@ func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client a
if err != nil {
cfgState.DeleteProvider(provider.String())
agentpool.Remove(&agentCfg)
return 0, gperr.Wrap(err, "failed to load routes")
return 0, fmt.Errorf("failed to load routes: %w", err)
}
return provider.NumRoutes(), nil

View File

@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/rs/zerolog/log"
gperr "github.com/yusing/goutils/errs"
_ "github.com/yusing/goutils/apitypes"
@@ -36,18 +37,18 @@ func Containers(c *gin.Context) {
serveHTTP[Container](c, GetContainers)
}
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, error) {
errs := gperr.NewBuilder("failed to get containers")
containers := make([]Container, 0)
for server, dockerClient := range dockerClients {
for name, dockerClient := range dockerClients {
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
if err != nil {
errs.Add(err)
errs.AddSubject(err, name)
continue
}
for _, cont := range conts.Items {
containers = append(containers, Container{
Server: server,
Server: name,
Name: cont.Names[0],
ID: cont.ID,
Image: cont.Image,
@@ -59,11 +60,10 @@ func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Containe
return containers[i].Name < containers[j].Name
})
if err := errs.Error(); err != nil {
gperr.LogError("failed to get containers", err)
if len(containers) == 0 {
return nil, err
if len(containers) > 0 {
log.Err(err).Msg("failed to get containers from some servers")
return containers, nil
}
return containers, nil
}
return containers, nil
return containers, errs.Error()
}

View File

@@ -59,7 +59,7 @@ func Info(c *gin.Context) {
serveHTTP[dockerInfo](c, GetDockerInfo)
}
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, gperr.Error) {
func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerInfo, error) {
errs := gperr.NewBuilder("failed to get docker info")
dockerInfos := make([]dockerInfo, len(dockerClients))
@@ -67,7 +67,7 @@ func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerIn
for name, dockerClient := range dockerClients {
info, err := dockerClient.Info(ctx, client.InfoOptions{})
if err != nil {
errs.Add(err)
errs.AddSubject(err, name)
continue
}
info.Info.Name = name

View File

@@ -10,7 +10,7 @@ import (
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/route/routes"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/types"
apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/httpheaders"
@@ -44,7 +44,7 @@ func Stats(c *gin.Context) {
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
if !ok {
var route types.Route
route, ok = routes.GetIncludeExcluded(id)
route, ok = entrypoint.FromCtx(c.Request.Context()).GetRoute(id)
if ok {
cont := route.ContainerInfo()
if cont == nil {

View File

@@ -8,7 +8,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/docker"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
)
@@ -39,7 +38,7 @@ func handleResult[V any, T ResultType[V]](c *gin.Context, errs error, result T)
c.JSON(http.StatusOK, result)
}
func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, error)) {
dockerClients := docker.Clients()
defer closeAllClients(dockerClients)

View File

@@ -1171,7 +1171,10 @@
"200": {
"description": "Health info by route name",
"schema": {
"$ref": "#/definitions/HealthMap"
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/HealthStatusString"
}
}
},
"403": {
@@ -1219,6 +1222,12 @@
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
}
},
"x-id": "categories",
@@ -1337,6 +1346,12 @@
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
}
},
"x-id": "items",
@@ -2820,43 +2835,6 @@
"operationId": "tail"
}
},
"/reload": {
"post": {
"description": "Reload config",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"v1"
],
"summary": "Reload config",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/SuccessResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
}
},
"x-id": "reload",
"operationId": "reload"
}
},
"/route/by_provider": {
"get": {
"description": "List routes by provider",
@@ -4071,14 +4049,6 @@
"x-nullable": false,
"x-omitempty": false
},
"HealthMap": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/HealthStatusString"
},
"x-nullable": false,
"x-omitempty": false
},
"HealthStatusString": {
"type": "string",
"enum": [
@@ -5329,7 +5299,6 @@
"x-omitempty": false
},
"bind": {
"description": "for TCP and UDP routes, bind address to listen on",
"type": "string",
"x-nullable": true
},

View File

@@ -419,10 +419,6 @@ definitions:
url:
type: string
type: object
HealthMap:
additionalProperties:
$ref: '#/definitions/HealthStatusString'
type: object
HealthStatusString:
enum:
- unknown
@@ -1007,7 +1003,6 @@ definitions:
alias:
type: string
bind:
description: for TCP and UDP routes, bind address to listen on
type: string
x-nullable: true
container:
@@ -1807,12 +1802,12 @@ definitions:
type: string
kernel_version:
type: string
load_avg_5m:
type: string
load_avg_15m:
type: string
load_avg_1m:
type: string
load_avg_5m:
type: string
mem_pct:
type: string
mem_total:
@@ -2675,7 +2670,9 @@ paths:
"200":
description: Health info by route name
schema:
$ref: '#/definitions/HealthMap'
additionalProperties:
$ref: '#/definitions/HealthStatusString'
type: object
"403":
description: Forbidden
schema:
@@ -2707,6 +2704,10 @@ paths:
description: Forbidden
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: List homepage categories
tags:
- homepage
@@ -2784,6 +2785,10 @@ paths:
description: Forbidden
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Homepage items
tags:
- homepage
@@ -3790,30 +3795,6 @@ paths:
- proxmox
- websocket
x-id: tail
/reload:
post:
consumes:
- application/json
description: Reload config
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/SuccessResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Reload config
tags:
- v1
x-id: reload
/route/{which}:
get:
consumes:

View File

@@ -5,9 +5,9 @@ import (
"net/http"
"github.com/gin-gonic/gin"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/homepage/icons"
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
"github.com/yusing/godoxy/internal/route/routes"
apitypes "github.com/yusing/goutils/apitypes"
_ "unsafe"
@@ -73,7 +73,11 @@ func FavIcon(c *gin.Context) {
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) {
// try with route.Icon
r, ok := routes.HTTP.Get(alias)
ep := entrypoint.FromCtx(ctx)
if ep == nil { // impossible, but just in case
return iconfetch.FetchResultWithErrorf(http.StatusInternalServerError, "entrypoint not initialized")
}
r, ok := ep.HTTPRoutes().Get(alias)
if !ok {
return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
}

View File

@@ -51,7 +51,7 @@ func Validate(c *gin.Context) {
c.JSON(http.StatusOK, apitypes.Success("file validated"))
}
func validateFile(fileType FileType, content []byte) gperr.Error {
func validateFile(fileType FileType, content []byte) error {
switch fileType {
case FileTypeConfig:
return config.Validate(content)

View File

@@ -5,11 +5,10 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/route/routes"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
_ "github.com/yusing/goutils/apitypes"
)
// @x-id "health"
@@ -19,16 +18,21 @@ import (
// @Tags v1,websocket
// @Accept json
// @Produce json
// @Success 200 {object} routes.HealthMap "Health info by route name"
// @Success 200 {object} map[string]types.HealthStatusString "Health info by route name"
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /health [get]
func Health(c *gin.Context) {
ep := entrypoint.FromCtx(c.Request.Context())
if ep == nil { // impossible, but just in case
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
return
}
if httpheaders.IsWebsocket(c.Request.Header) {
websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) {
return routes.GetHealthInfoSimple(), nil
return ep.GetHealthInfoSimple(), nil
})
} else {
c.JSON(http.StatusOK, routes.GetHealthInfoSimple())
c.JSON(http.StatusOK, ep.GetHealthInfoSimple())
}
}

View File

@@ -4,10 +4,11 @@ import (
"net/http"
"github.com/gin-gonic/gin"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/homepage"
"github.com/yusing/godoxy/internal/route/routes"
_ "github.com/yusing/goutils/apitypes"
apitypes "github.com/yusing/goutils/apitypes"
)
// @x-id "categories"
@@ -19,17 +20,23 @@ import (
// @Produce json
// @Success 200 {array} string
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/categories [get]
func Categories(c *gin.Context) {
c.JSON(http.StatusOK, HomepageCategories())
ep := entrypoint.FromCtx(c.Request.Context())
if ep == nil { // impossible, but just in case
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
return
}
c.JSON(http.StatusOK, HomepageCategories(ep))
}
func HomepageCategories() []string {
func HomepageCategories(ep entrypoint.Entrypoint) []string {
check := make(map[string]struct{})
categories := make([]string, 0)
categories = append(categories, homepage.CategoryAll)
categories = append(categories, homepage.CategoryFavorites)
for _, r := range routes.HTTP.Iter {
for _, r := range ep.HTTPRoutes().Iter {
item := r.HomepageItem()
if item.Category == "" {
continue

View File

@@ -10,8 +10,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/lithammer/fuzzysearch/fuzzy"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/homepage"
"github.com/yusing/godoxy/internal/route/routes"
apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
@@ -36,6 +36,7 @@ type HomepageItemsRequest struct {
// @Success 200 {object} homepage.Homepage
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /homepage/items [get]
func Items(c *gin.Context) {
var request HomepageItemsRequest
@@ -53,29 +54,35 @@ func Items(c *gin.Context) {
hostname = host
}
ep := entrypoint.FromCtx(c.Request.Context())
if ep == nil {
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not found in context", nil))
return
}
if httpheaders.IsWebsocket(c.Request.Header) {
websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) {
return HomepageItems(proto, hostname, &request), nil
return HomepageItems(ep, proto, hostname, &request), nil
})
} else {
c.JSON(http.StatusOK, HomepageItems(proto, hostname, &request))
c.JSON(http.StatusOK, HomepageItems(ep, proto, hostname, &request))
}
}
func HomepageItems(proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
func HomepageItems(ep entrypoint.Entrypoint, proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
switch proto {
case "http", "https":
default:
proto = "http"
}
hp := homepage.NewHomepageMap(routes.HTTP.Size())
hp := homepage.NewHomepageMap(ep.HTTPRoutes().Size())
if strings.Count(hostname, ".") > 1 {
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
}
for _, r := range routes.HTTP.Iter {
for _, r := range ep.HTTPRoutes().Iter {
if request.Provider != "" && r.ProviderName() != request.Provider {
continue
}

View File

@@ -113,7 +113,7 @@ func AllSystemInfo(c *gin.Context) {
data, err := systeminfo.Poller.GetRespData(req.Period, query)
if err != nil {
numErrs.Add(1)
return gperr.PrependSubject("Main server", err)
return gperr.PrependSubject(err, "Main server")
}
select {
case <-manager.Done():
@@ -133,7 +133,7 @@ func AllSystemInfo(c *gin.Context) {
data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded)
if err != nil {
numErrs.Add(1)
return gperr.PrependSubject("Agent "+a.Name, err)
return gperr.PrependSubject(err, "Agent "+a.Name)
}
select {
case <-manager.Done():
@@ -170,7 +170,7 @@ func AllSystemInfo(c *gin.Context) {
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
return
}
gperr.LogWarn("failed to get some system info", err)
log.Warn().Err(err).Msg("failed to get some system info")
}
}
}

View File

@@ -1,28 +0,0 @@
package v1
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/config"
apitypes "github.com/yusing/goutils/apitypes"
)
// @x-id "reload"
// @BasePath /api/v1
// @Summary Reload config
// @Description Reload config
// @Tags v1
// @Accept json
// @Produce json
// @Success 200 {object} apitypes.SuccessResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /reload [post]
func Reload(c *gin.Context) {
if err := config.Reload(); err != nil {
c.Error(apitypes.InternalServerError(err, "failed to reload config"))
return
}
c.JSON(http.StatusOK, apitypes.Success("config reloaded"))
}

View File

@@ -4,10 +4,10 @@ import (
"net/http"
"github.com/gin-gonic/gin"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/route"
"github.com/yusing/godoxy/internal/route/routes"
_ "github.com/yusing/goutils/apitypes"
apitypes "github.com/yusing/goutils/apitypes"
)
type RoutesByProvider map[string][]route.Route
@@ -24,5 +24,10 @@ type RoutesByProvider map[string][]route.Route
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /route/by_provider [get]
func ByProvider(c *gin.Context) {
c.JSON(http.StatusOK, routes.ByProvider())
ep := entrypoint.FromCtx(c.Request.Context())
if ep == nil { // impossible, but just in case
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
return
}
c.JSON(http.StatusOK, ep.RoutesByProvider())
}

View File

@@ -1,6 +1,7 @@
package routeApi
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
@@ -54,16 +55,16 @@ type PlaygroundResponse struct {
MatchedRules []string `json:"matchedRules"`
FinalRequest FinalRequest `json:"finalRequest"`
FinalResponse FinalResponse `json:"finalResponse"`
ExecutionError gperr.Error `json:"executionError,omitempty"`
ExecutionError error `json:"executionError,omitempty"` // we need the structured error, not the plain string
UpstreamCalled bool `json:"upstreamCalled"`
} // @name PlaygroundResponse
type ParsedRule struct {
Name string `json:"name"`
On string `json:"on"`
Do string `json:"do"`
ValidationError gperr.Error `json:"validationError,omitempty"`
IsResponseRule bool `json:"isResponseRule"`
Name string `json:"name"`
On string `json:"on"`
Do string `json:"do"`
ValidationError error `json:"validationError,omitempty"` // we need the structured error, not the plain string
IsResponseRule bool `json:"isResponseRule"`
} // @name ParsedRule
type FinalRequest struct {
@@ -138,7 +139,7 @@ func Playground(c *gin.Context) {
// Execute rules
matchedRules := []string{}
upstreamCalled := false
var executionError gperr.Error
var executionError error
// Variables to capture modified request state
var finalReqMethod, finalReqPath, finalReqHost string
@@ -244,21 +245,23 @@ func Playground(c *gin.Context) {
c.JSON(http.StatusOK, response)
}
func handlerWithRecover(w http.ResponseWriter, r *http.Request, h http.HandlerFunc, outErr *gperr.Error) {
func handlerWithRecover(w http.ResponseWriter, r *http.Request, h http.HandlerFunc, outErr *error) {
defer func() {
if r := recover(); r != nil {
if outErr != nil {
*outErr = gperr.Errorf("panic during rule execution: %v", r)
*outErr = fmt.Errorf("panic during rule execution: %v", r)
}
}
}()
h(w, r)
}
func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, gperr.Error) {
func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
var parsedRules []ParsedRule
var rulesList rules.Rules
var valErrs gperr.Builder
// Parse each rule individually to capture per-rule errors
for _, rawRule := range rawRules {
var rule rules.Rule
@@ -284,7 +287,11 @@ func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, gperr.Error) {
// Determine if valid
isValid := onErr == nil && doErr == nil
validationErr := gperr.Join(gperr.PrependSubject("on", onErr), gperr.PrependSubject("do", doErr))
var validationErr error
if !isValid {
validationErr = gperr.Join(gperr.PrependSubject(onErr, "on"), gperr.PrependSubject(doErr, "do"))
valErrs.Add(validationErr)
}
parsedRules = append(parsedRules, ParsedRule{
Name: name,
@@ -300,7 +307,7 @@ func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, gperr.Error) {
}
}
return parsedRules, rulesList, nil
return parsedRules, rulesList, valErrs.Error()
}
func createMockRequest(mock MockRequest) *http.Request {

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/route/routes"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
apitypes "github.com/yusing/goutils/apitypes"
)
@@ -32,7 +32,13 @@ func Route(c *gin.Context) {
return
}
route, ok := routes.GetIncludeExcluded(request.Which)
ep := entrypoint.FromCtx(c.Request.Context())
if ep == nil { // impossible, but just in case
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
return
}
route, ok := ep.GetRoute(request.Which)
if ok {
c.JSON(http.StatusOK, route)
return

View File

@@ -6,8 +6,8 @@ import (
"time"
"github.com/gin-gonic/gin"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/route"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
@@ -32,14 +32,16 @@ func Routes(c *gin.Context) {
return
}
ep := entrypoint.FromCtx(c.Request.Context())
provider := c.Query("provider")
if provider == "" {
c.JSON(http.StatusOK, slices.Collect(routes.IterAll))
c.JSON(http.StatusOK, slices.Collect(ep.IterRoutes))
return
}
rts := make([]types.Route, 0, routes.NumAllRoutes())
for r := range routes.IterAll {
rts := make([]types.Route, 0, ep.NumRoutes())
for r := range ep.IterRoutes {
if r.ProviderName() == provider {
rts = append(rts, r)
}
@@ -48,17 +50,19 @@ func Routes(c *gin.Context) {
}
func RoutesWS(c *gin.Context) {
ep := entrypoint.FromCtx(c.Request.Context())
provider := c.Query("provider")
if provider == "" {
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
return slices.Collect(routes.IterAll), nil
return slices.Collect(ep.IterRoutes), nil
})
return
}
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
rts := make([]types.Route, 0, routes.NumAllRoutes())
for r := range routes.IterAll {
rts := make([]types.Route, 0, ep.NumRoutes())
for r := range ep.IterRoutes {
if r.ProviderName() == provider {
rts = append(rts, r)
}

View File

@@ -17,7 +17,6 @@ import (
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/utils"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http"
"golang.org/x/oauth2"
"golang.org/x/time/rate"
@@ -76,8 +75,8 @@ const (
var (
errMissingIDToken = errors.New("missing id_token field from oauth token")
ErrMissingOAuthToken = gperr.New("missing oauth token")
ErrInvalidOAuthToken = gperr.New("invalid oauth token")
ErrMissingOAuthToken = errors.New("missing oauth token")
ErrInvalidOAuthToken = errors.New("invalid oauth token")
)
// generateState generates a random string for OIDC state.

View File

@@ -1,6 +1,7 @@
package auth
import (
"errors"
"fmt"
"net/http"
"time"
@@ -8,16 +9,12 @@ import (
"github.com/bytedance/sonic"
"github.com/golang-jwt/jwt/v5"
"github.com/yusing/godoxy/internal/common"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http"
strutils "github.com/yusing/goutils/strings"
"golang.org/x/crypto/bcrypt"
)
var (
ErrInvalidUsername = gperr.New("invalid username")
ErrInvalidPassword = gperr.New("invalid password")
)
var ErrInvalidUsername = errors.New("invalid username")
type (
UserPassAuth struct {
@@ -94,9 +91,9 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
case !token.Valid:
return ErrInvalidSessionToken
case claims.Username != auth.username:
return ErrUserNotAllowed.Subject(claims.Username)
return fmt.Errorf("%w: %s", ErrUserNotAllowed, claims.Username)
case claims.ExpiresAt.Before(time.Now()):
return gperr.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
return fmt.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
}
return nil
@@ -140,10 +137,10 @@ func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request)
func (auth *UserPassAuth) validatePassword(user, pass string) error {
if user != auth.username {
return ErrInvalidUsername.Subject(user)
return ErrInvalidUsername
}
if err := bcrypt.CompareHashAndPassword(auth.pwdHash, []byte(pass)); err != nil {
return ErrInvalidPassword.With(err).Subject(pass)
return err
}
return nil
}

View File

@@ -27,7 +27,7 @@ func TestUserPassValidateCredentials(t *testing.T) {
err := auth.validatePassword("username", "password")
expect.NoError(t, err)
err = auth.validatePassword("username", "wrong-password")
expect.ErrorIs(t, ErrInvalidPassword, err)
expect.ErrorIs(t, bcrypt.ErrMismatchedHashAndPassword, err)
err = auth.validatePassword("wrong-username", "password")
expect.ErrorIs(t, ErrInvalidUsername, err)
}

View File

@@ -1,20 +1,20 @@
package auth
import (
"errors"
"net"
"net/http"
"strings"
"time"
"github.com/yusing/godoxy/internal/common"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
var (
ErrMissingSessionToken = gperr.New("missing session token")
ErrInvalidSessionToken = gperr.New("invalid session token")
ErrUserNotAllowed = gperr.New("user not allowed")
ErrMissingSessionToken = errors.New("missing session token")
ErrInvalidSessionToken = errors.New("invalid session token")
ErrUserNotAllowed = errors.New("user not allowed")
)
func IsFrontend(r *http.Request) bool {

View File

@@ -66,13 +66,13 @@ const (
var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
// Validate implements the utils.CustomValidator interface.
func (cfg *Config) Validate() gperr.Error {
// Validate implements the serialization.CustomValidator interface.
func (cfg *Config) Validate() error {
seenPaths := make(map[string]int) // path -> provider idx (0 for main, 1+ for extras)
return cfg.validate(seenPaths)
}
func (cfg *ConfigExtra) Validate() gperr.Error {
func (cfg *ConfigExtra) Validate() error {
return nil // done by main config's validate
}
@@ -80,7 +80,7 @@ func (cfg *ConfigExtra) AsConfig() *Config {
return (*Config)(cfg)
}
func (cfg *Config) validate(seenPaths map[string]int) gperr.Error {
func (cfg *Config) validate(seenPaths map[string]int) error {
if cfg.Provider == "" {
cfg.Provider = ProviderLocal
}
@@ -157,7 +157,7 @@ func (cfg *Config) validate(seenPaths map[string]int) gperr.Error {
cfg.Extra[i].AsConfig().idx = i + 1
err := cfg.Extra[i].AsConfig().validate(seenPaths)
if err != nil {
b.Add(err.Subjectf("extra[%d]", i))
b.AddSubjectf(err, "extra[%d]", i)
}
}
}
@@ -179,10 +179,10 @@ func (cfg *Config) GetLegoConfig() (*User, *lego.Config, error) {
log.Info().Err(err).Msg("failed to load ACME private key, generating a now one")
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, gperr.New("generate ACME private key").With(err)
return nil, nil, fmt.Errorf("generate ACME private key: %w", err)
}
if err = cfg.SaveACMEKey(privKey); err != nil {
return nil, nil, gperr.New("save ACME private key").With(err)
return nil, nil, fmt.Errorf("save ACME private key: %w", err)
}
}
}
@@ -206,7 +206,7 @@ func (cfg *Config) GetLegoConfig() (*User, *lego.Config, error) {
if len(cfg.CACerts) > 0 {
certPool, err := lego.CreateCertPool(cfg.CACerts, true)
if err != nil {
return nil, nil, gperr.New("failed to create cert pool").With(err)
return nil, nil, fmt.Errorf("failed to create cert pool: %w", err)
}
legoCfg.HTTPClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = certPool
}

View File

@@ -150,7 +150,7 @@ func (p *Provider) GetName() string {
}
func (p *Provider) fmtError(err error) error {
return gperr.PrependSubject(fmt.Sprintf("provider: %s", p.GetName()), err)
return gperr.PrependSubject(err, "provider: "+p.GetName())
}
func (p *Provider) GetCertPath() string {
@@ -216,7 +216,7 @@ func (p *Provider) ObtainCertIfNotExistsAll() error {
for _, provider := range p.allProviders() {
errs.Go(func() error {
if err := provider.obtainCertIfNotExists(); err != nil {
return fmt.Errorf("failed to obtain cert for %s: %w", provider.GetName(), err)
return gperr.PrependSubject(err, provider.GetName())
}
return nil
})
@@ -475,7 +475,7 @@ func (p *Provider) scheduleRenewal(parent task.Parent) {
renewed, err := p.renew(renewMode)
if err != nil {
gperr.LogWarn("autocert: cert renew failed", p.fmtError(err))
log.Warn().Err(p.fmtError(err)).Msg("autocert: cert renew failed")
notif.Notify(&notif.LogMessage{
Level: zerolog.ErrorLevel,
Title: fmt.Sprintf("SSL certificate renewal failed for %s", p.GetName()),
@@ -494,7 +494,7 @@ func (p *Provider) scheduleRenewal(parent task.Parent) {
// Reset on success
if err := p.ClearLastFailure(); err != nil {
gperr.LogWarn("autocert: failed to clear last failure", p.fmtError(err))
log.Warn().Err(p.fmtError(err)).Msg("autocert: failed to clear last failure")
}
timer.Reset(time.Until(p.ShouldRenewOn()))
}

View File

@@ -3,11 +3,10 @@ package autocert
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]strutils.Redacted) (challenge.Provider, gperr.Error)
type Generator func(map[string]strutils.Redacted) (challenge.Provider, error)
var Providers = make(map[string]Generator)
@@ -15,7 +14,7 @@ func DNSProvider[CT any, PT challenge.Provider](
defaultCfg func() *CT,
newProvider func(*CT) (PT, error),
) Generator {
return func(opt map[string]strutils.Redacted) (challenge.Provider, gperr.Error) {
return func(opt map[string]strutils.Redacted) (challenge.Provider, error) {
cfg := defaultCfg()
if len(opt) > 0 {
err := serialization.MapUnmarshalValidate(serialization.ToSerializedObject(opt), &cfg)
@@ -24,6 +23,6 @@ func DNSProvider[CT any, PT challenge.Provider](
}
}
p, pErr := newProvider(cfg)
return p, gperr.Wrap(pErr)
return p, pErr
}
}

View File

@@ -4,7 +4,7 @@ import (
gperr "github.com/yusing/goutils/errs"
)
func (p *Provider) setupExtraProviders() gperr.Error {
func (p *Provider) setupExtraProviders() error {
p.sniMatcher = sniMatcher{}
if len(p.cfg.Extra) == 0 {
return nil

View File

@@ -0,0 +1,16 @@
package autocert
import "context"
type ContextKey struct{}
func SetCtx(ctx interface{ SetValue(any, any) }, p Provider) {
ctx.SetValue(ContextKey{}, p)
}
func FromCtx(ctx context.Context) Provider {
if provider, ok := ctx.Value(ContextKey{}).(Provider); ok {
return provider
}
return nil
}

View File

@@ -7,7 +7,6 @@ import (
)
type Provider interface {
Setup() error
GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error)
ScheduleRenewalAll(task.Parent)
ObtainCertAll() error

View File

@@ -54,7 +54,7 @@ type State interface {
Task() *task.Task
Context() context.Context
Value() *Config
EntrypointHandler() http.Handler
Entrypoint() entrypoint.Entrypoint
ShortLinkMatcher() config.ShortLinkMatcher
AutoCertProvider() server.CertProvider
LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool)
@@ -62,6 +62,12 @@ type State interface {
IterProviders() iter.Seq2[string, types.RouteProvider]
StartProviders() error
NumProviders() int
// Lifecycle management
StartAPIServers()
StartMetrics()
FlushTmpLog()
}
```
@@ -214,12 +220,15 @@ Configuration supports hot-reloading via editing `config/config.yml`.
- `internal/acl` - Access control configuration
- `internal/autocert` - SSL certificate management
- `internal/entrypoint` - HTTP entrypoint setup
- `internal/entrypoint` - HTTP entrypoint setup (now via interface)
- `internal/route/provider` - Route providers (Docker, file, agent)
- `internal/maxmind` - GeoIP configuration
- `internal/notif` - Notification providers
- `internal/proxmox` - LXC container management
- `internal/homepage/types` - Dashboard configuration
- `internal/api` - REST API servers
- `internal/metrics/systeminfo` - System metrics polling
- `internal/metrics/uptime` - Uptime tracking
- `github.com/yusing/goutils/task` - Object lifecycle management
### External dependencies
@@ -312,5 +321,8 @@ for name, provider := range config.GetState().IterProviders() {
```go
state := config.GetState()
http.Handle("/", state.EntrypointHandler())
// Get entrypoint interface for route management
ep := state.Entrypoint()
// Add routes directly to entrypoint
ep.AddRoute(route)
```

View File

@@ -7,14 +7,13 @@ import (
"time"
"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/notif"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/watcher"
"github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/strings/ansi"
"github.com/yusing/goutils/task"
)
@@ -34,7 +33,7 @@ You may run "ls-config" to show or dump the current config.`
)
func logNotifyError(action string, err error) {
gperr.LogError("config "+action+" error", err)
log.Error().Err(err).Msg("config " + action + " error")
notif.Notify(&notif.LogMessage{
Level: zerolog.ErrorLevel,
Title: fmt.Sprintf("Config %s error", action),
@@ -43,7 +42,7 @@ func logNotifyError(action string, err error) {
}
func logNotifyWarn(action string, err error) {
gperr.LogWarn("config "+action+" error", err)
log.Warn().Err(err).Msg("config " + action + " warning")
notif.Notify(&notif.LogMessage{
Level: zerolog.WarnLevel,
Title: fmt.Sprintf("Config %s warning", action),
@@ -60,20 +59,30 @@ func Load() error {
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
// disable pool logging temporary since we already have pretty logging
routes.HTTP.DisableLog(true)
routes.Stream.DisableLog(true)
initErr := state.InitFromFile(common.ConfigPath)
if initErr != nil {
// if error is critical, notify and return it without starting providers
var criticalErr CriticalError
if errors.As(initErr, &criticalErr) {
logNotifyError("init", criticalErr.err)
return criticalErr.err
}
}
// disable pool logging temporary since we already have pretty logging
state.Entrypoint().DisablePoolsLog(true)
defer func() {
routes.HTTP.DisableLog(false)
routes.Stream.DisableLog(false)
state.Entrypoint().DisablePoolsLog(false)
}()
initErr := state.InitFromFile(common.ConfigPath)
err := errors.Join(initErr, state.StartProviders())
if err != nil {
logNotifyError("init", err)
}
state.StartAPIServers()
state.StartMetrics()
SetState(state)
// flush temporary log
@@ -81,7 +90,7 @@ func Load() error {
return nil
}
func Reload() gperr.Error {
func Reload() error {
// avoid race between config change and API reload request
reloadMu.Lock()
defer reloadMu.Unlock()
@@ -108,7 +117,9 @@ func Reload() gperr.Error {
logNotifyError("start providers", err)
return nil // continue
}
StartProxyServers()
newState.StartAPIServers()
newState.StartMetrics()
return nil
}
@@ -118,7 +129,7 @@ func WatchChanges() {
t,
configEventFlushInterval,
OnConfigChange,
func(err gperr.Error) {
func(err error) {
logNotifyError("reload", err)
},
)
@@ -142,16 +153,3 @@ func OnConfigChange(ev []events.Event) {
panic(err)
}
}
func StartProxyServers() {
cfg := GetState()
server.StartServer(cfg.Task(), server.Options{
Name: "proxy",
CertProvider: cfg.AutoCertProvider(),
HTTPAddr: common.ProxyHTTPAddr,
HTTPSAddr: common.ProxyHTTPSAddr,
Handler: cfg.EntrypointHandler(),
ACL: cfg.Value().ACL,
SupportProxyProtocol: cfg.Value().Entrypoint.SupportProxyProtocol,
})
}

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"io/fs"
"iter"
"net/http"
"os"
"strconv"
"strings"
@@ -18,14 +17,20 @@ import (
"github.com/goccy/go-yaml"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog"
"github.com/yusing/godoxy/internal/acl"
acl "github.com/yusing/godoxy/internal/acl/types"
"github.com/yusing/godoxy/internal/agentpool"
"github.com/yusing/godoxy/internal/api"
"github.com/yusing/godoxy/internal/autocert"
autocertctx "github.com/yusing/godoxy/internal/autocert/types"
"github.com/yusing/godoxy/internal/common"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/entrypoint"
entrypointctx "github.com/yusing/godoxy/internal/entrypoint/types"
homepage "github.com/yusing/godoxy/internal/homepage/types"
"github.com/yusing/godoxy/internal/logging"
"github.com/yusing/godoxy/internal/maxmind"
"github.com/yusing/godoxy/internal/metrics/systeminfo"
"github.com/yusing/godoxy/internal/metrics/uptime"
"github.com/yusing/godoxy/internal/notif"
route "github.com/yusing/godoxy/internal/route/provider"
"github.com/yusing/godoxy/internal/serialization"
@@ -40,7 +45,7 @@ type state struct {
providers *xsync.Map[string, types.RouteProvider]
autocertProvider *autocert.Provider
entrypoint entrypoint.Entrypoint
entrypoint *entrypoint.Entrypoint
task *task.Task
@@ -50,14 +55,25 @@ type state struct {
tmpLog zerolog.Logger
}
type CriticalError struct {
err error
}
func (e CriticalError) Error() string {
return e.err.Error()
}
func (e CriticalError) Unwrap() error {
return e.err
}
func NewState() config.State {
tmpLogBuf := bytes.NewBuffer(make([]byte, 0, 4096))
return &state{
providers: xsync.NewMap[string, types.RouteProvider](),
entrypoint: entrypoint.NewEntrypoint(),
task: task.RootTask("config", false),
tmpLogBuf: tmpLogBuf,
tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf),
providers: xsync.NewMap[string, types.RouteProvider](),
task: task.RootTask("config", false),
tmpLogBuf: tmpLogBuf,
tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf),
}
}
@@ -73,7 +89,6 @@ func SetState(state config.State) {
cfg := state.Value()
config.ActiveState.Store(state)
entrypoint.ActiveConfig.Store(&cfg.Entrypoint)
homepage.ActiveConfig.Store(&cfg.Homepage)
if autocertProvider := state.AutoCertProvider(); autocertProvider != nil {
autocert.ActiveProvider.Store(autocertProvider.(*autocert.Provider))
@@ -96,7 +111,7 @@ func (state *state) InitFromFile(filename string) error {
if errors.Is(err, fs.ErrNotExist) {
state.Config = config.DefaultConfig()
} else {
return err
return CriticalError{err}
}
}
return state.Init(data)
@@ -105,7 +120,7 @@ func (state *state) InitFromFile(filename string) error {
func (state *state) Init(data []byte) error {
err := serialization.UnmarshalValidate(data, &state.Config, yaml.Unmarshal)
if err != nil {
return err
return CriticalError{err}
}
g := gperr.NewGroup("config load error")
@@ -117,7 +132,9 @@ func (state *state) Init(data []byte) error {
// these won't benefit from running on goroutines
errs.Add(state.initNotification())
errs.Add(state.initACL())
errs.Add(state.initEntrypoint())
if err := state.initEntrypoint(); err != nil {
errs.Add(CriticalError{err})
}
errs.Add(state.loadRouteProviders())
return errs.Error()
}
@@ -134,8 +151,8 @@ func (state *state) Value() *config.Config {
return &state.Config
}
func (state *state) EntrypointHandler() http.Handler {
return &state.entrypoint
func (state *state) Entrypoint() entrypointctx.Entrypoint {
return state.entrypoint
}
func (state *state) ShortLinkMatcher() config.ShortLinkMatcher {
@@ -190,6 +207,29 @@ func (state *state) FlushTmpLog() {
state.tmpLogBuf.Reset()
}
func (state *state) StartAPIServers() {
// API Handler needs to start after auth is initialized.
server.StartServer(state.task.Subtask("api_server", false), server.Options{
Name: "api",
HTTPAddr: common.APIHTTPAddr,
Handler: api.NewHandler(true),
})
// Local API Handler is used for unauthenticated access.
if common.LocalAPIHTTPAddr != "" {
server.StartServer(state.task.Subtask("local_api_server", false), server.Options{
Name: "local_api",
HTTPAddr: common.LocalAPIHTTPAddr,
Handler: api.NewHandler(false),
})
}
}
func (state *state) StartMetrics() {
systeminfo.Poller.Start(state.task)
uptime.Poller.Start(state.task)
}
// initACL initializes the ACL.
func (state *state) initACL() error {
if !state.ACL.Valid() {
@@ -199,7 +239,7 @@ func (state *state) initACL() error {
if err != nil {
return err
}
state.task.SetValue(acl.ContextKey{}, state.ACL)
acl.SetCtx(state.task, state.ACL)
return nil
}
@@ -207,6 +247,7 @@ func (state *state) initEntrypoint() error {
epCfg := state.Config.Entrypoint
matchDomains := state.MatchDomains
state.entrypoint = entrypoint.NewEntrypoint(state.task, &epCfg)
state.entrypoint.SetFindRouteDomains(matchDomains)
state.entrypoint.SetNotFoundRules(epCfg.Rules.NotFound)
@@ -220,6 +261,8 @@ func (state *state) initEntrypoint() error {
}
}
entrypointctx.SetCtx(state.task, state.entrypoint)
errs := gperr.NewBuilder("entrypoint error")
errs.Add(state.entrypoint.SetMiddlewares(epCfg.Middlewares))
errs.Add(state.entrypoint.SetAccessLogger(state.task, epCfg.AccessLog))
@@ -296,6 +339,7 @@ func (state *state) initAutoCert() error {
p.PrintCertExpiriesAll()
state.autocertProvider = p
autocertctx.SetCtx(state.task, p)
return nil
}
@@ -309,7 +353,7 @@ func (state *state) initProxmox() error {
for _, cfg := range proxmoxCfg {
errs.Go(func() error {
if err := cfg.Init(state.task.Context()); err != nil {
return err.Subject(cfg.URL)
return gperr.PrependSubject(err, cfg.URL)
}
return nil
})
@@ -333,7 +377,7 @@ func (state *state) loadRouteProviders() error {
for _, a := range providers.Agents {
agentErrs.Go(func() error {
if err := a.Init(state.task.Context()); err != nil {
return gperr.PrependSubject(a.String(), err)
return gperr.PrependSubject(err, a.String())
}
agentpool.Add(a)
return nil
@@ -351,7 +395,7 @@ func (state *state) loadRouteProviders() error {
for _, filename := range providers.Files {
p, err := route.NewFileProvider(filename)
if err != nil {
errs.Add(gperr.PrependSubject(filename, err))
errs.Add(gperr.PrependSubject(err, filename))
return err
}
registerProvider(p)
@@ -376,7 +420,7 @@ func (state *state) loadRouteProviders() error {
for _, p := range state.providers.Range {
loadErrs.Go(func() error {
if err := p.LoadRoutes(); err != nil {
return err.Subject(p.String())
return gperr.PrependSubject(err, p.String())
}
resultsMu.Lock()
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())

View File

@@ -8,14 +8,13 @@ import (
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/acl"
"github.com/yusing/godoxy/internal/autocert"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/entrypoint"
homepage "github.com/yusing/godoxy/internal/homepage/types"
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
"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"
)
type (
@@ -42,7 +41,7 @@ type (
}
)
func Validate(data []byte) gperr.Error {
func Validate(data []byte) error {
var model Config
return serialization.UnmarshalValidate(data, &model, yaml.Unmarshal)
}

View File

@@ -6,6 +6,7 @@ import (
"iter"
"net/http"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/synk"
@@ -21,7 +22,7 @@ type State interface {
Value() *Config
EntrypointHandler() http.Handler
Entrypoint() entrypoint.Entrypoint
ShortLinkMatcher() ShortLinkMatcher
AutoCertProvider() server.CertProvider
@@ -32,6 +33,9 @@ type State interface {
StartProviders() error
FlushTmpLog()
StartAPIServers()
StartMetrics()
}
type ShortLinkMatcher interface {

View File

@@ -1,12 +1,12 @@
module github.com/yusing/godoxy/internal/dnsproviders
go 1.25.6
go 1.25.7
replace github.com/yusing/godoxy => ../..
require (
github.com/go-acme/lego/v4 v4.31.0
github.com/yusing/godoxy v0.25.2
github.com/yusing/godoxy v0.25.3
)
require (
@@ -32,7 +32,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -49,7 +49,7 @@ require (
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/gotify/server/v2 v2.8.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
@@ -57,7 +57,7 @@ require (
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/linode/linodego v1.64.0 // indirect
github.com/linode/linodego v1.65.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/maxatome/go-testdeep v1.14.0 // indirect
@@ -65,8 +65,8 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/goinwx v0.12.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.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
@@ -79,15 +79,15 @@ 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.26.1 // indirect
github.com/vultr/govultr/v3 v3.27.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusing/gointernals v0.1.16 // indirect
github.com/yusing/gointernals v0.1.18 // indirect
github.com/yusing/goutils v0.7.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
@@ -98,8 +98,8 @@ require (
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/api v0.263.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/api v0.265.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.1 // indirect

View File

@@ -60,8 +60,8 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
@@ -105,8 +105,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@@ -131,8 +131,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY=
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k=
github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -150,10 +150,10 @@ github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0 h1:eMzyN+jGJbxG4ut278uwIsUo9XacXc711lFjhKnaUso=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.107.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0 h1:t34IpOa+8NfmjkU8bdWtYrLrmr346/FGhu8FlpJDQok=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.107.0/go.mod h1:p95/OxVsdx71I2Qrck1GtIS87sRxcTRKXzUi5nWm9NY=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.0 h1:SgXDi/vitC+FA9jPl7T7i0d7kiC1JMFuS2FTlpg3B7o=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.108.0/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.0 h1:rOYbG56bYaW+skuCvzZyFoTPPXXUIOPhuQkilf6tkqo=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.108.0/go.mod h1:LTEIH1X6CBKyDtOT7CTTTgvvb2ANtGS7vLkEGt5zdog=
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=
@@ -193,30 +193,30 @@ 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.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8=
github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
github.com/yusing/gointernals v0.1.18 h1:ou8/0tPURUgAOBJu3TN/iWF4S/5ZYQaap+rVkaJNUMw=
github.com/yusing/gointernals v0.1.18/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
github.com/yusing/goutils v0.7.0 h1:I5hd8GwZ+3WZqFPK0tWqek1Q5MY6Xg29hKZcwwQi4SY=
github.com/yusing/goutils v0.7.0/go.mod h1:CtF/KFH4q8jkr7cvBpkaExnudE0lLu8sLe43F73Bn5Q=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
@@ -249,14 +249,14 @@ golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
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.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk=
google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU=
google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -1,6 +1,7 @@
package docker
import (
"errors"
"fmt"
"strconv"
"strings"
@@ -11,7 +12,7 @@ import (
strutils "github.com/yusing/goutils/strings"
)
var ErrInvalidLabel = gperr.New("invalid label")
var ErrInvalidLabel = errors.New("invalid label")
const nsProxyDot = NSProxy + "."
@@ -23,7 +24,7 @@ var refPrefixes = func() []string {
return prefixes
}()
func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, gperr.Error) {
func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, error) {
nestedMap := make(types.LabelMap)
errs := gperr.NewBuilder("labels error")
@@ -35,7 +36,7 @@ func ParseLabels(labels map[string]string, aliases ...string) (types.LabelMap, g
continue
}
if len(parts) == 1 {
errs.Add(ErrInvalidLabel.Subject(lbl))
errs.AddSubject(ErrInvalidLabel, lbl)
continue
}
parts = parts[1:]

View File

@@ -1,10 +1,10 @@
# Entrypoint
The entrypoint package provides the main HTTP entry point for GoDoxy, handling domain-based routing, middleware application, short link matching, and access logging.
The entrypoint package provides the main HTTP entry point for GoDoxy, handling domain-based routing, middleware application, short link matching, access logging, and HTTP server lifecycle management.
## Overview
The entrypoint package implements the primary HTTP handler that receives all incoming requests, determines the target route based on hostname, applies middleware, and forwards requests to the appropriate route handler.
The entrypoint package implements the primary HTTP handler that receives all incoming requests, manages the lifecycle of HTTP servers, determines the target route based on hostname, applies middleware, and forwards requests to the appropriate route handler.
### Key Features
@@ -14,103 +14,350 @@ The entrypoint package implements the primary HTTP handler that receives all inc
- Access logging for all requests
- Configurable not-found handling
- Per-domain route resolution
- HTTP server management (HTTP/HTTPS)
- Route pool abstractions via [`PoolLike`](internal/entrypoint/types/entrypoint.go:27) and [`RWPoolLike`](internal/entrypoint/types/entrypoint.go:33) interfaces
## Architecture
### Primary Consumers
```mermaid
graph TD
A[HTTP Request] --> B[Entrypoint Handler]
B --> C{Access Logger?}
C -->|Yes| D[Wrap Response Recorder]
C -->|No| E[Skip Logging]
- **HTTP servers**: Per-listen-addr servers dispatch requests to routes
- **Route providers**: Register routes via [`StartAddRoute`](internal/entrypoint/routes.go:48)
- **Configuration layer**: Validates and applies middleware/access-logging config
D --> F[Find Route by Host]
E --> F
### Non-goals
F --> G{Route Found?}
G -->|Yes| H{Middleware?}
G -->|No| I{Short Link?}
I -->|Yes| J[Short Link Handler]
I -->|No| K{Not Found Handler?}
K -->|Yes| L[Not Found Handler]
K -->|No| M[Serve 404]
- Does not implement route discovery (delegates to providers)
- Does not handle TLS certificate management (delegates to autocert)
- Does not implement health checks (delegates to `internal/health/monitor`)
- Does not manage TCP/UDP listeners directly (only HTTP/HTTPS via `goutils/server`)
H -->|Yes| N[Apply Middleware]
H -->|No| O[Direct Route]
N --> O
### Stability
O --> P[Route ServeHTTP]
P --> Q[Response]
L --> R[404 Response]
J --> Q
M --> R
```
## Core Components
### Entrypoint Structure
```go
type Entrypoint struct {
middleware *middleware.Middleware
notFoundHandler http.Handler
accessLogger accesslog.AccessLogger
findRouteFunc func(host string) types.HTTPRoute
shortLinkTree *ShortLinkMatcher
}
```
### Active Config
```go
var ActiveConfig atomic.Pointer[entrypoint.Config]
```
Internal package with stable core interfaces. The [`Entrypoint`](internal/entrypoint/types/entrypoint.go:7) interface is the public contract.
## Public API
### Creation
### Entrypoint Interface
```go
// NewEntrypoint creates a new entrypoint instance.
func NewEntrypoint() Entrypoint
type Entrypoint interface {
// Server capabilities
SupportProxyProtocol() bool
DisablePoolsLog(v bool)
// Route registry access
GetRoute(alias string) (types.Route, bool)
StartAddRoute(r types.Route) error
IterRoutes(yield func(r types.Route) bool)
NumRoutes() int
RoutesByProvider() map[string][]types.Route
// Route pool accessors
HTTPRoutes() PoolLike[types.HTTPRoute]
StreamRoutes() PoolLike[types.StreamRoute]
ExcludedRoutes() RWPoolLike[types.Route]
// Health info queries
GetHealthInfo() map[string]types.HealthInfo
GetHealthInfoWithoutDetail() map[string]types.HealthInfoWithoutDetail
GetHealthInfoSimple() map[string]types.HealthStatus
// Configuration
SetFindRouteDomains(domains []string)
SetMiddlewares(mws []map[string]any) error
SetNotFoundRules(rules rules.Rules)
SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) error
// Context integration
ShortLinkMatcher() *ShortLinkMatcher
}
```
### Pool Interfaces
```go
type PoolLike[Route types.Route] interface {
Get(alias string) (Route, bool)
Iter(yield func(alias string, r Route) bool)
Size() int
}
type RWPoolLike[Route types.Route] interface {
PoolLike[Route]
Add(r Route)
Del(r Route)
}
```
### Configuration
```go
// SetFindRouteDomains configures domain-based route lookup.
func (ep *Entrypoint) SetFindRouteDomains(domains []string)
// SetMiddlewares loads and configures middleware chain.
func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error
// SetNotFoundRules configures the not-found handler.
func (ep *Entrypoint) SetNotFoundRules(rules rules.Rules)
// SetAccessLogger initializes access logging.
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) error
// ShortLinkMatcher returns the short link matcher.
func (ep *Entrypoint) ShortLinkMatcher() *ShortLinkMatcher
type Config struct {
SupportProxyProtocol bool `json:"support_proxy_protocol"`
Rules struct {
NotFound rules.Rules `json:"not_found"`
} `json:"rules"`
Middlewares []map[string]any `json:"middlewares"`
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
}
```
### Request Handling
### Context Functions
```go
// ServeHTTP is the main HTTP handler.
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request)
// FindRoute looks up a route by hostname.
func (ep *Entrypoint) FindRoute(s string) types.HTTPRoute
func SetCtx(ctx interface{ SetValue(any, any) }, ep Entrypoint)
func FromCtx(ctx context.Context) Entrypoint
```
## Usage
## Architecture
### Core Components
```mermaid
classDiagram
class Entrypoint {
+task *task.new_task
+cfg *Config
+middleware *middleware.Middleware
+notFoundHandler http.Handler
+accessLogger AccessLogger
+findRouteFunc findRouteFunc
+shortLinkMatcher *ShortLinkMatcher
+streamRoutes *pool.Pool[types.StreamRoute]
+excludedRoutes *pool.Pool[types.Route]
+servers *xsync.Map[string, *httpServer]
+SupportProxyProtocol() bool
+StartAddRoute(r) error
+IterRoutes(yield)
+HTTPRoutes() PoolLike
}
class httpServer {
+routes *pool.Pool[types.HTTPRoute]
+ServeHTTP(w, r)
+AddRoute(route)
+DelRoute(route)
+FindRoute(s) types.HTTPRoute
}
class PoolLike {
<<interface>>
+Get(alias) (Route, bool)
+Iter(yield) bool
+Size() int
}
class RWPoolLike {
<<interface>>
+PoolLike
+Add(r Route)
+Del(r Route)
}
class ShortLinkMatcher {
+fqdnRoutes *xsync.Map[string, string]
+subdomainRoutes *xsync.Map[string, struct{}]
+ServeHTTP(w, r)
+AddRoute(alias)
+DelRoute(alias)
+SetDefaultDomainSuffix(suffix)
}
Entrypoint --> httpServer : manages
Entrypoint --> ShortLinkMatcher : owns
Entrypoint --> PoolLike : HTTPRoutes()
Entrypoint --> RWPoolLike : ExcludedRoutes()
httpServer --> PoolLike : routes pool
```
### Request Processing Pipeline
```mermaid
flowchart TD
A[HTTP Request] --> B[Find Route by Host]
B --> C{Route Found?}
C -->|Yes| D{Middleware?}
C -->|No| E{Short Link?}
E -->|Yes| F[Short Link Handler]
E -->|No| G{Not Found Handler?}
G -->|Yes| H[Not Found Handler]
G -->|No| I[Serve 404]
D -->|Yes| J[Apply Middleware Chain]
D -->|No| K[Direct Route Handler]
J --> K
K --> L[Route ServeHTTP]
L --> M[Response]
F --> M
H --> N[404 Response]
I --> N
```
### Server Lifecycle
```mermaid
stateDiagram-v2
[*] --> Empty: NewEntrypoint()
Empty --> Listening: StartAddRoute()
Listening --> Listening: StartAddRoute()
Listening --> Listening: delHTTPRoute()
Listening --> [*]: Cancel()
Listening --> AddingServer: addHTTPRoute()
AddingServer --> Listening: Server starts
note right of Listening
servers map: addr -> httpServer
For HTTPS, routes are added to ProxyHTTPSAddr
Default routes added to both HTTP and HTTPS
end note
```
## Data Flow
```mermaid
sequenceDiagram
participant Client
participant httpServer
participant Entrypoint
participant Middleware
participant Route
Client->>httpServer: GET /path
httpServer->>Entrypoint: FindRoute(host)
alt Route Found
Entrypoint-->>httpServer: HTTPRoute
httpServer->>Middleware: ServeHTTP(routeHandler)
alt Has Middleware
Middleware->>Middleware: Process Chain
end
Middleware->>Route: Forward Request
Route-->>Middleware: Response
Middleware-->>httpServer: Response
else Short Link (go.example.com/alias)
httpServer->>ShortLinkMatcher: Match short code
ShortLinkMatcher-->>httpServer: Redirect
else Not Found
httpServer->>NotFoundHandler: Serve 404
NotFoundHandler-->>httpServer: 404 Page
end
httpServer-->>Client: Response
```
## Route Registry
Routes are managed per-entrypoint:
```go
// Adding a route (main entry point for providers)
if err := ep.StartAddRoute(route); err != nil {
return err
}
// Iterating all routes including excluded
ep.IterRoutes(func(r types.Route) bool {
log.Info().Str("alias", r.Name()).Msg("route")
return true // continue iteration
})
// Querying by alias
route, ok := ep.GetRoute("myapp")
// Grouping by provider
byProvider := ep.RoutesByProvider()
```
## Configuration Surface
### Config Source
Environment variables and YAML config file:
```yaml
entrypoint:
support_proxy_protocol: true
middlewares:
- rate_limit:
requests_per_second: 100
rules:
not_found:
# not-found rules configuration
access_log:
path: /var/log/godoxy/access.log
```
### Environment Variables
| Variable | Description |
| ------------------------------ | ----------------------------- |
| `PROXY_SUPPORT_PROXY_PROTOCOL` | Enable PROXY protocol support |
## Dependency and Integration Map
| Dependency | Purpose |
| ---------------------------------- | --------------------------- |
| `internal/route` | Route types and handlers |
| `internal/route/rules` | Not-found rules processing |
| `internal/logging/accesslog` | Request logging |
| `internal/net/gphttp/middleware` | Middleware chain |
| `internal/types` | Route and health types |
| `github.com/puzpuzpuz/xsync/v4` | Concurrent server map |
| `github.com/yusing/goutils/pool` | Route pool implementations |
| `github.com/yusing/goutils/task` | Lifecycle management |
| `github.com/yusing/goutils/server` | HTTP/HTTPS server lifecycle |
## Observability
### Logs
| Level | Context | Description |
| ------- | --------------------- | ----------------------- |
| `DEBUG` | `route`, `listen_url` | Route addition/removal |
| `DEBUG` | `addr`, `proto` | Server lifecycle |
| `ERROR` | `route`, `listen_url` | Server startup failures |
### Metrics
Route metrics exposed via [`GetHealthInfo`](internal/entrypoint/query.go:10) methods:
```go
// Health info for all routes
healthMap := ep.GetHealthInfo()
// {
// "myapp": {Status: "healthy", Uptime: 3600, Latency: 5ms},
// "excluded-route": {Status: "unknown", Detail: "n/a"},
// }
```
## Security Considerations
- Route lookup is read-only from route pools
- Middleware chain is applied per-request
- Proxy protocol support must be explicitly enabled
- Access logger captures request metadata before processing
- Short link matching is limited to configured domains
## Failure Modes and Recovery
| Failure | Behavior | Recovery |
| --------------------- | ------------------------------- | ---------------------------- |
| Server bind fails | Error returned, route not added | Fix port/address conflict |
| Route start fails | Route excluded, error logged | Fix route configuration |
| Middleware load fails | SetMiddlewares returns error | Fix middleware configuration |
| Context cancelled | All servers stopped gracefully | Restart entrypoint |
## Usage Examples
### Basic Setup
```go
ep := entrypoint.NewEntrypoint()
ep := entrypoint.NewEntrypoint(parent, &entrypoint.Config{
SupportProxyProtocol: false,
})
// Configure domain matching
ep.SetFindRouteDomains([]string{".example.com", "example.com"})
@@ -120,7 +367,7 @@ err := ep.SetMiddlewares([]map[string]any{
{"rate_limit": map[string]any{"requests_per_second": 100}},
})
if err != nil {
log.Fatal(err)
return err
}
// Configure access logging
@@ -128,181 +375,58 @@ err = ep.SetAccessLogger(parent, &accesslog.RequestLoggerConfig{
Path: "/var/log/godoxy/access.log",
})
if err != nil {
log.Fatal(err)
return err
}
// Start server
http.ListenAndServe(":80", &ep)
```
### Route Lookup Logic
The entrypoint uses multiple strategies to find routes:
1. **Subdomain Matching**: For `sub.domain.com`, looks for `sub`
1. **Exact Match**: Looks for the full hostname
1. **Port Stripping**: Strips port from host if present
### Route Querying
```go
func findRouteAnyDomain(host string) types.HTTPRoute {
// Try subdomain (everything before first dot)
idx := strings.IndexByte(host, '.')
if idx != -1 {
target := host[:idx]
if r, ok := routes.HTTP.Get(target); ok {
return r
}
}
// Iterate all routes including excluded
ep.IterRoutes(func(r types.Route) bool {
log.Info().
Str("alias", r.Name()).
Str("provider", r.ProviderName()).
Bool("excluded", r.ShouldExclude()).
Msg("route")
return true // continue iteration
})
// Try exact match
if r, ok := routes.HTTP.Get(host); ok {
return r
}
// Try stripping port
if before, _, ok := strings.Cut(host, ":"); ok {
if r, ok := routes.HTTP.Get(before); ok {
return r
}
}
return nil
// Get health info for all routes
healthMap := ep.GetHealthInfoSimple()
for alias, status := range healthMap {
log.Info().Str("alias", alias).Str("status", string(status)).Msg("health")
}
```
### Short Links
### Route Addition
Short links use a special `.short` domain:
Routes are typically added by providers via `StartAddRoute`:
```go
// Request to: https://abc.short.example.com
// Looks for route with alias "abc"
if strings.EqualFold(host, common.ShortLinkPrefix) {
// Handle short link
ep.shortLinkTree.ServeHTTP(w, r)
// StartAddRoute handles route registration and server creation
if err := ep.StartAddRoute(route); err != nil {
return err
}
```
## Data Flow
### Context Integration
```mermaid
sequenceDiagram
participant Client
participant Entrypoint
participant Middleware
participant Route
participant Logger
Client->>Entrypoint: GET /path
Entrypoint->>Entrypoint: FindRoute(host)
alt Route Found
Entrypoint->>Logger: Get ResponseRecorder
Logger-->>Entrypoint: Recorder
Entrypoint->>Middleware: ServeHTTP(routeHandler)
alt Has Middleware
Middleware->>Middleware: Process Chain
end
Middleware->>Route: Forward Request
Route-->>Middleware: Response
Middleware-->>Entrypoint: Response
else Short Link
Entrypoint->>ShortLinkTree: Match short code
ShortLinkTree-->>Entrypoint: Redirect
else Not Found
Entrypoint->>NotFoundHandler: Serve 404
NotFoundHandler-->>Entrypoint: 404 Page
end
Entrypoint->>Logger: Log Request
Logger-->>Entrypoint: Complete
Entrypoint-->>Client: Response
```
## Not-Found Handling
When no route is found, the entrypoint:
1. Attempts to serve a static error page file
1. Logs the 404 request
1. Falls back to the configured error page
1. Returns 404 status code
Routes can access the entrypoint from request context:
```go
func (ep *Entrypoint) serveNotFound(w http.ResponseWriter, r *http.Request) {
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
log.Error().
Str("method", r.Method).
Str("url", r.URL.String()).
Str("remote", r.RemoteAddr).
Msgf("not found: %s", r.Host)
// Set entrypoint in context (typically during initialization)
entrypoint.SetCtx(task, ep)
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
if ok {
w.WriteHeader(http.StatusNotFound)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(errorPage)
} else {
http.NotFound(w, r)
}
}
// Get entrypoint from context
if ep := entrypoint.FromCtx(r.Context()); ep != nil {
route, ok := ep.GetRoute("alias")
}
```
## Configuration Structure
## Testing Notes
```go
type Config struct {
Middlewares []map[string]any `json:"middlewares"`
Rules rules.Rules `json:"rules"`
AccessLog *accesslog.RequestLoggerConfig `json:"access_log"`
}
```
## Middleware Integration
The entrypoint supports middleware chains configured via YAML:
```yaml
entrypoint:
middlewares:
- use: rate_limit
average: 100
burst: 200
bypass:
- remote 192.168.1.0/24
- use: redirect_http
```
## Access Logging
Access logging wraps the response recorder to capture:
- Request method and URL
- Response status code
- Response size
- Request duration
- Client IP address
```go
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if ep.accessLogger != nil {
rec := accesslog.GetResponseRecorder(w)
w = rec
defer func() {
ep.accessLogger.Log(r, rec.Response())
accesslog.PutResponseRecorder(rec)
}()
}
// ... handle request
}
```
## Integration Points
The entrypoint integrates with:
- **Route Registry**: HTTP route lookup
- **Middleware**: Request processing chain
- **AccessLog**: Request logging
- **ErrorPage**: 404 error pages
- **ShortLink**: Short link handling
- Benchmark tests in [`entrypoint_benchmark_test.go`](internal/entrypoint/entrypoint_benchmark_test.go)
- Integration tests in [`entrypoint_test.go`](internal/entrypoint/entrypoint_test.go)
- Mock route pools for unit testing
- Short link tests in [`shortlink_test.go`](internal/entrypoint/shortlink_test.go)

View File

@@ -4,44 +4,112 @@ import (
"net/http"
"strings"
"sync/atomic"
"testing"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/net/gphttp/middleware/errorpage"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/route/rules"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/pool"
"github.com/yusing/goutils/task"
)
type HTTPRoutes interface {
Get(alias string) (types.HTTPRoute, bool)
}
type findRouteFunc func(HTTPRoutes, string) types.HTTPRoute
type Entrypoint struct {
middleware *middleware.Middleware
notFoundHandler http.Handler
accessLogger accesslog.AccessLogger
findRouteFunc func(host string) types.HTTPRoute
shortLinkTree *ShortLinkMatcher
task *task.Task
cfg *Config
middleware *middleware.Middleware
notFoundHandler http.Handler
accessLogger accesslog.AccessLogger
findRouteFunc findRouteFunc
shortLinkMatcher *ShortLinkMatcher
streamRoutes *pool.Pool[types.StreamRoute]
excludedRoutes *pool.Pool[types.Route]
// this only affects future http servers creation
httpPoolDisableLog atomic.Bool
servers *xsync.Map[string, *httpServer] // listen addr -> server
}
// nil-safe
var ActiveConfig atomic.Pointer[entrypoint.Config]
var _ entrypoint.Entrypoint = &Entrypoint{}
func init() {
// make sure it's not nil
ActiveConfig.Store(&entrypoint.Config{})
var emptyCfg Config
func NewTestEntrypoint(t testing.TB, cfg *Config) *Entrypoint {
t.Helper()
testTask := task.GetTestTask(t)
ep := NewEntrypoint(testTask, cfg)
entrypoint.SetCtx(testTask, ep)
return ep
}
func NewEntrypoint() Entrypoint {
return Entrypoint{
findRouteFunc: findRouteAnyDomain,
shortLinkTree: newShortLinkTree(),
func NewEntrypoint(parent task.Parent, cfg *Config) *Entrypoint {
if cfg == nil {
cfg = &emptyCfg
}
ep := &Entrypoint{
task: parent.Subtask("entrypoint", false),
cfg: cfg,
findRouteFunc: findRouteAnyDomain,
shortLinkMatcher: newShortLinkMatcher(),
streamRoutes: pool.New[types.StreamRoute]("stream_routes"),
excludedRoutes: pool.New[types.Route]("excluded_routes"),
servers: xsync.NewMap[string, *httpServer](),
}
return ep
}
func (ep *Entrypoint) Task() *task.Task {
return ep.task
}
func (ep *Entrypoint) SupportProxyProtocol() bool {
return ep.cfg.SupportProxyProtocol
}
func (ep *Entrypoint) DisablePoolsLog(v bool) {
ep.httpPoolDisableLog.Store(v)
// apply to all running http servers
for _, srv := range ep.servers.Range {
srv.routes.DisableLog(v)
}
// apply to other pools
ep.streamRoutes.DisableLog(v)
ep.excludedRoutes.DisableLog(v)
}
func (ep *Entrypoint) ShortLinkMatcher() *ShortLinkMatcher {
return ep.shortLinkTree
return ep.shortLinkMatcher
}
func (ep *Entrypoint) HTTPRoutes() entrypoint.PoolLike[types.HTTPRoute] {
return newHTTPPoolAdapter(ep)
}
func (ep *Entrypoint) StreamRoutes() entrypoint.PoolLike[types.StreamRoute] {
return ep.streamRoutes
}
func (ep *Entrypoint) ExcludedRoutes() entrypoint.RWPoolLike[types.Route] {
return ep.excludedRoutes
}
func (ep *Entrypoint) GetServer(addr string) (HTTPServer, bool) {
return ep.servers.Load(addr)
}
func (ep *Entrypoint) SetFindRouteDomains(domains []string) {
@@ -74,7 +142,7 @@ func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error {
}
func (ep *Entrypoint) SetNotFoundRules(rules rules.Rules) {
ep.notFoundHandler = rules.BuildHandler(http.HandlerFunc(ep.serveNotFound))
ep.notFoundHandler = rules.BuildHandler(serveNotFound)
}
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) (err error) {
@@ -91,111 +159,39 @@ func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.Request
return err
}
func (ep *Entrypoint) FindRoute(s string) types.HTTPRoute {
return ep.findRouteFunc(s)
}
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if ep.accessLogger != nil {
rec := accesslog.GetResponseRecorder(w)
w = rec
defer func() {
ep.accessLogger.LogRequest(r, rec.Response())
accesslog.PutResponseRecorder(rec)
}()
}
route := ep.findRouteFunc(r.Host)
switch {
case route != nil:
r = routes.WithRouteContext(r, route)
if ep.middleware != nil {
ep.middleware.ServeHTTP(route.ServeHTTP, w, r)
} else {
route.ServeHTTP(w, r)
}
case ep.tryHandleShortLink(w, r):
return
case ep.notFoundHandler != nil:
ep.notFoundHandler.ServeHTTP(w, r)
default:
ep.serveNotFound(w, r)
}
}
func (ep *Entrypoint) tryHandleShortLink(w http.ResponseWriter, r *http.Request) (handled bool) {
host := r.Host
if before, _, ok := strings.Cut(host, ":"); ok {
host = before
}
if strings.EqualFold(host, common.ShortLinkPrefix) {
if ep.middleware != nil {
ep.middleware.ServeHTTP(ep.shortLinkTree.ServeHTTP, w, r)
} else {
ep.shortLinkTree.ServeHTTP(w, r)
}
return true
}
return false
}
func (ep *Entrypoint) serveNotFound(w http.ResponseWriter, r *http.Request) {
// Why use StatusNotFound instead of StatusBadRequest or StatusBadGateway?
// On nginx, when route for domain does not exist, it returns StatusBadGateway.
// Then scraper / scanners will know the subdomain is invalid.
// With StatusNotFound, they won't know whether it's the path, or the subdomain that is invalid.
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
log.Error().
Str("method", r.Method).
Str("url", r.URL.String()).
Str("remote", r.RemoteAddr).
Msgf("not found: %s", r.Host)
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
if ok {
w.WriteHeader(http.StatusNotFound)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if _, err := w.Write(errorPage); err != nil {
log.Err(err).Msg("failed to write error page")
}
} else {
http.NotFound(w, r)
}
}
}
func findRouteAnyDomain(host string) types.HTTPRoute {
func findRouteAnyDomain(routes HTTPRoutes, host string) types.HTTPRoute {
idx := strings.IndexByte(host, '.')
if idx != -1 {
target := host[:idx]
if r, ok := routes.HTTP.Get(target); ok {
if r, ok := routes.Get(target); ok {
return r
}
}
if r, ok := routes.HTTP.Get(host); ok {
if r, ok := routes.Get(host); ok {
return r
}
// try striping the trailing :port from the host
if before, _, ok := strings.Cut(host, ":"); ok {
if r, ok := routes.HTTP.Get(before); ok {
if r, ok := routes.Get(before); ok {
return r
}
}
return nil
}
func findRouteByDomains(domains []string) func(host string) types.HTTPRoute {
return func(host string) types.HTTPRoute {
func findRouteByDomains(domains []string) func(routes HTTPRoutes, host string) types.HTTPRoute {
return func(routes HTTPRoutes, host string) types.HTTPRoute {
host, _, _ = strings.Cut(host, ":") // strip the trailing :port
for _, domain := range domains {
if target, ok := strings.CutSuffix(host, domain); ok {
if r, ok := routes.HTTP.Get(target); ok {
if r, ok := routes.Get(target); ok {
return r
}
}
}
// fallback to exact match
if r, ok := routes.HTTP.Get(host); ok {
if r, ok := routes.Get(host); ok {
return r
}
return nil

View File

@@ -10,9 +10,11 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/yusing/godoxy/internal/common"
. "github.com/yusing/godoxy/internal/entrypoint"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/route"
"github.com/yusing/godoxy/internal/route/routes"
routeTypes "github.com/yusing/godoxy/internal/route/types"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/task"
@@ -48,13 +50,15 @@ func (t noopTransport) RoundTrip(req *http.Request) (*http.Response, error) {
}
func BenchmarkEntrypointReal(b *testing.B) {
var ep Entrypoint
task := task.GetTestTask(b)
ep := NewEntrypoint(task, nil)
req := http.Request{
Method: "GET",
URL: &url.URL{Path: "/", RawPath: "/"},
Host: "test.domain.tld",
}
ep.SetFindRouteDomains([]string{})
entrypoint.SetCtx(task, ep)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", "1")
@@ -77,48 +81,48 @@ func BenchmarkEntrypointReal(b *testing.B) {
b.Fatal(err)
}
r := &route.Route{
r, err := route.NewStartedTestRoute(b, &route.Route{
Alias: "test",
Scheme: routeTypes.SchemeHTTP,
Host: host,
Port: route.Port{Proxy: portInt},
HealthCheck: types.HealthCheckConfig{Disable: true},
}
})
err = r.Validate()
if err != nil {
b.Fatal(err)
}
err = r.Start(task.RootTask("test", false))
if err != nil {
b.Fatal(err)
}
require.NoError(b, err)
require.False(b, r.ShouldExclude())
var w noopResponseWriter
server, ok := ep.GetServer(common.ProxyHTTPAddr)
if !ok {
b.Fatal("server not found")
}
b.ResetTimer()
for b.Loop() {
ep.ServeHTTP(&w, &req)
// if w.statusCode != http.StatusOK {
// b.Fatalf("status code is not 200: %d", w.statusCode)
// }
// if string(w.written) != "1" {
// b.Fatalf("written is not 1: %s", string(w.written))
// }
server.ServeHTTP(&w, &req)
if w.statusCode != http.StatusOK {
b.Fatalf("status code is not 200: %d", w.statusCode)
}
if string(w.written) != "1" {
b.Fatalf("written is not 1: %s", string(w.written))
}
}
}
func BenchmarkEntrypoint(b *testing.B) {
var ep Entrypoint
task := task.GetTestTask(b)
ep := NewEntrypoint(task, nil)
req := http.Request{
Method: "GET",
URL: &url.URL{Path: "/", RawPath: "/"},
Host: "test.domain.tld",
}
ep.SetFindRouteDomains([]string{})
entrypoint.SetCtx(task, ep)
r := &route.Route{
r, err := route.NewStartedTestRoute(b, &route.Route{
Alias: "test",
Scheme: routeTypes.SchemeHTTP,
Host: "localhost",
@@ -128,29 +132,23 @@ func BenchmarkEntrypoint(b *testing.B) {
HealthCheck: types.HealthCheckConfig{
Disable: true,
},
}
})
err := r.Validate()
if err != nil {
b.Fatal(err)
}
require.NoError(b, err)
require.False(b, r.ShouldExclude())
err = r.Start(task.RootTask("test", false))
if err != nil {
b.Fatal(err)
}
rev, ok := routes.HTTP.Get("test")
if !ok {
b.Fatal("route not found")
}
rev.(types.ReverseProxyRoute).ReverseProxy().Transport = noopTransport{}
r.(types.ReverseProxyRoute).ReverseProxy().Transport = noopTransport{}
var w noopResponseWriter
server, ok := ep.GetServer(common.ProxyHTTPAddr)
if !ok {
b.Fatal("server not found")
}
b.ResetTimer()
for b.Loop() {
ep.ServeHTTP(&w, &req)
server.ServeHTTP(&w, &req)
if w.statusCode != http.StatusOK {
b.Fatalf("status code is not 200: %d", w.statusCode)
}

View File

@@ -3,48 +3,70 @@ package entrypoint_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
. "github.com/yusing/godoxy/internal/entrypoint"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/route"
"github.com/yusing/godoxy/internal/route/routes"
routeTypes "github.com/yusing/godoxy/internal/route/types"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/task"
expect "github.com/yusing/goutils/testing"
)
var ep = NewEntrypoint()
func addRoute(t *testing.T, alias string) {
t.Helper()
func addRoute(alias string) {
routes.HTTP.Add(&route.ReveseProxyRoute{
Route: &route.Route{
Alias: alias,
Port: route.Port{
Proxy: 80,
},
ep := entrypoint.FromCtx(task.GetTestTask(t).Context())
require.NotNil(t, ep)
_, err := route.NewStartedTestRoute(t, &route.Route{
Alias: alias,
Scheme: routeTypes.SchemeHTTP,
Port: route.Port{
Listening: 1000,
Proxy: 8080,
},
HealthCheck: types.HealthCheckConfig{
Disable: true,
},
})
if err != nil {
t.Fatal(err)
}
route, ok := ep.HTTPRoutes().Get(alias)
require.True(t, ok, "route not found")
require.NotNil(t, route)
}
func run(t *testing.T, match []string, noMatch []string) {
func run(t *testing.T, ep *Entrypoint, match []string, noMatch []string) {
t.Helper()
t.Cleanup(routes.Clear)
t.Cleanup(func() { ep.SetFindRouteDomains(nil) })
server, ok := ep.GetServer(":1000")
require.True(t, ok, "server not found")
require.NotNil(t, server)
for _, test := range match {
t.Run(test, func(t *testing.T) {
found := ep.FindRoute(test)
expect.NotNil(t, found)
route := server.FindRoute(test)
assert.NotNil(t, route)
})
}
for _, test := range noMatch {
t.Run(test, func(t *testing.T) {
found := ep.FindRoute(test)
expect.Nil(t, found)
found, ok := ep.HTTPRoutes().Get(test)
assert.False(t, ok)
assert.Nil(t, found)
})
}
}
func TestFindRouteAnyDomain(t *testing.T) {
addRoute("app1")
ep := NewTestEntrypoint(t, nil)
addRoute(t, "app1")
tests := []string{
"app1.com",
@@ -58,10 +80,12 @@ func TestFindRouteAnyDomain(t *testing.T) {
"app2.sub.domain.com",
}
run(t, tests, testsNoMatch)
run(t, ep, tests, testsNoMatch)
}
func TestFindRouteExactHostMatch(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
tests := []string{
"app2.com",
"app2.domain.com",
@@ -75,19 +99,20 @@ func TestFindRouteExactHostMatch(t *testing.T) {
}
for _, test := range tests {
addRoute(test)
addRoute(t, test)
}
run(t, tests, testsNoMatch)
run(t, ep, tests, testsNoMatch)
}
func TestFindRouteByDomains(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
ep.SetFindRouteDomains([]string{
".domain.com",
".sub.domain.com",
})
addRoute("app1")
addRoute(t, "app1")
tests := []string{
"app1.domain.com",
@@ -103,16 +128,17 @@ func TestFindRouteByDomains(t *testing.T) {
"app2.sub.domain.com",
}
run(t, tests, testsNoMatch)
run(t, ep, tests, testsNoMatch)
}
func TestFindRouteByDomainsExactMatch(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
ep.SetFindRouteDomains([]string{
".domain.com",
".sub.domain.com",
})
addRoute("app1.foo.bar")
addRoute(t, "app1.foo.bar")
tests := []string{
"app1.foo.bar", // exact match
@@ -126,13 +152,14 @@ func TestFindRouteByDomainsExactMatch(t *testing.T) {
"app1.sub.domain.com",
}
run(t, tests, testsNoMatch)
run(t, ep, tests, testsNoMatch)
}
func TestFindRouteWithPort(t *testing.T) {
t.Run("AnyDomain", func(t *testing.T) {
addRoute("app1")
addRoute("app2.com")
ep := NewTestEntrypoint(t, nil)
addRoute(t, "app1")
addRoute(t, "app2.com")
tests := []string{
"app1:8080",
@@ -144,16 +171,17 @@ func TestFindRouteWithPort(t *testing.T) {
"app2.co",
"app2.co:8080",
}
run(t, tests, testsNoMatch)
run(t, ep, tests, testsNoMatch)
})
t.Run("ByDomains", func(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
ep.SetFindRouteDomains([]string{
".domain.com",
})
addRoute("app1")
addRoute("app2")
addRoute("app3.domain.com")
addRoute(t, "app1")
addRoute(t, "app2")
addRoute(t, "app3.domain.com")
tests := []string{
"app1.domain.com:8080",
@@ -169,6 +197,120 @@ func TestFindRouteWithPort(t *testing.T) {
"app3.domain.co",
"app3.domain.co:8080",
}
run(t, tests, testsNoMatch)
run(t, ep, tests, testsNoMatch)
})
}
func TestHealthInfoQueries(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
// Add routes without health monitors (default case)
addRoute(t, "app1")
addRoute(t, "app2")
// Test GetHealthInfo
t.Run("GetHealthInfo", func(t *testing.T) {
info := ep.GetHealthInfo()
expect.Equal(t, 2, len(info))
for _, health := range info {
expect.Equal(t, types.StatusUnknown, health.Status)
expect.Equal(t, "n/a", health.Detail)
}
})
// Test GetHealthInfoWithoutDetail
t.Run("GetHealthInfoWithoutDetail", func(t *testing.T) {
info := ep.GetHealthInfoWithoutDetail()
expect.Equal(t, 2, len(info))
for _, health := range info {
expect.Equal(t, types.StatusUnknown, health.Status)
}
})
// Test GetHealthInfoSimple
t.Run("GetHealthInfoSimple", func(t *testing.T) {
info := ep.GetHealthInfoSimple()
expect.Equal(t, 2, len(info))
for _, status := range info {
expect.Equal(t, types.StatusUnknown, status)
}
})
}
func TestRoutesByProvider(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
// Add routes with provider info
addRoute(t, "app1")
addRoute(t, "app2")
byProvider := ep.RoutesByProvider()
expect.Equal(t, 1, len(byProvider)) // All routes are from same implicit provider
routes, ok := byProvider[""]
expect.True(t, ok)
expect.Equal(t, 2, len(routes))
}
func TestNumRoutes(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
expect.Equal(t, 0, ep.NumRoutes())
addRoute(t, "app1")
expect.Equal(t, 1, ep.NumRoutes())
addRoute(t, "app2")
expect.Equal(t, 2, ep.NumRoutes())
}
func TestIterRoutes(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
addRoute(t, "app1")
addRoute(t, "app2")
addRoute(t, "app3")
count := 0
for r := range ep.IterRoutes {
count++
expect.NotNil(t, r)
}
expect.Equal(t, 3, count)
}
func TestGetRoute(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
// Route not found case
_, ok := ep.GetRoute("nonexistent")
expect.False(t, ok)
addRoute(t, "app1")
route, ok := ep.GetRoute("app1")
expect.True(t, ok)
expect.NotNil(t, route)
}
func TestHTTPRoutesPool(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
pool := ep.HTTPRoutes()
expect.Equal(t, 0, pool.Size())
addRoute(t, "app1")
expect.Equal(t, 1, pool.Size())
// Verify route is accessible
route, ok := pool.Get("app1")
expect.True(t, ok)
expect.NotNil(t, route)
}
func TestExcludedRoutesPool(t *testing.T) {
ep := NewTestEntrypoint(t, nil)
excludedPool := ep.ExcludedRoutes()
expect.Equal(t, 0, excludedPool.Size())
}

View File

@@ -0,0 +1,51 @@
package entrypoint
import (
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/types"
)
// httpPoolAdapter implements the PoolLike interface for the HTTP routes.
type httpPoolAdapter struct {
ep *Entrypoint
}
func newHTTPPoolAdapter(ep *Entrypoint) httpPoolAdapter {
return httpPoolAdapter{ep: ep}
}
func (h httpPoolAdapter) Iter(yield func(alias string, route types.HTTPRoute) bool) {
for addr, srv := range h.ep.servers.Range {
// default routes are added to both HTTP and HTTPS servers, we don't need to iterate over them twice.
if addr == common.ProxyHTTPSAddr {
continue
}
for alias, route := range srv.routes.Iter {
if !yield(alias, route) {
return
}
}
}
}
func (h httpPoolAdapter) Get(alias string) (types.HTTPRoute, bool) {
for addr, srv := range h.ep.servers.Range {
if addr == common.ProxyHTTPSAddr {
continue
}
if route, ok := srv.routes.Get(alias); ok {
return route, true
}
}
return nil, false
}
func (h httpPoolAdapter) Size() (n int) {
for addr, srv := range h.ep.servers.Range {
if addr == common.ProxyHTTPSAddr {
continue
}
n += srv.routes.Size()
}
return
}

View File

@@ -0,0 +1,173 @@
package entrypoint
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/rs/zerolog/log"
acl "github.com/yusing/godoxy/internal/acl/types"
autocert "github.com/yusing/godoxy/internal/autocert/types"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/net/gphttp/middleware/errorpage"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/pool"
"github.com/yusing/goutils/server"
)
// httpServer is a server that listens on a given address and serves HTTP routes.
type HTTPServer interface {
Listen(addr string, proto HTTPProto) error
AddRoute(route types.HTTPRoute)
DelRoute(route types.HTTPRoute)
FindRoute(s string) types.HTTPRoute
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
type httpServer struct {
ep *Entrypoint
stopFunc func(reason any)
addr string
routes *pool.Pool[types.HTTPRoute]
}
type HTTPProto string
const (
HTTPProtoHTTP HTTPProto = "http"
HTTPProtoHTTPS HTTPProto = "https"
)
func NewHTTPServer(ep *Entrypoint) HTTPServer {
return newHTTPServer(ep)
}
func newHTTPServer(ep *Entrypoint) *httpServer {
return &httpServer{ep: ep}
}
// Listen starts the server and stop when entrypoint is stopped.
func (srv *httpServer) Listen(addr string, proto HTTPProto) error {
if srv.addr != "" {
return errors.New("server already started")
}
opts := server.Options{
Name: addr,
Handler: srv,
ACL: acl.FromCtx(srv.ep.task.Context()),
SupportProxyProtocol: srv.ep.cfg.SupportProxyProtocol,
}
switch proto {
case HTTPProtoHTTP:
opts.HTTPAddr = addr
case HTTPProtoHTTPS:
opts.HTTPSAddr = addr
opts.CertProvider = autocert.FromCtx(srv.ep.task.Context())
}
task := srv.ep.task.Subtask("http_server", false)
_, err := server.StartServer(task, opts)
if err != nil {
return err
}
srv.stopFunc = task.FinishAndWait
srv.addr = addr
srv.routes = pool.New[types.HTTPRoute](fmt.Sprintf("[%s] %s", proto, addr))
srv.routes.DisableLog(srv.ep.httpPoolDisableLog.Load())
return nil
}
func (srv *httpServer) Close() {
if srv.stopFunc == nil {
return
}
srv.stopFunc(nil)
}
func (srv *httpServer) AddRoute(route types.HTTPRoute) {
srv.routes.Add(route)
}
func (srv *httpServer) DelRoute(route types.HTTPRoute) {
srv.routes.Del(route)
}
func (srv *httpServer) FindRoute(s string) types.HTTPRoute {
return srv.ep.findRouteFunc(srv.routes, s)
}
func (srv *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if srv.ep.accessLogger != nil {
rec := accesslog.GetResponseRecorder(w)
w = rec
defer func() {
srv.ep.accessLogger.LogRequest(r, rec.Response())
accesslog.PutResponseRecorder(rec)
}()
}
route := srv.ep.findRouteFunc(srv.routes, r.Host)
switch {
case route != nil:
r = routes.WithRouteContext(r, route)
if srv.ep.middleware != nil {
srv.ep.middleware.ServeHTTP(route.ServeHTTP, w, r)
} else {
route.ServeHTTP(w, r)
}
case srv.tryHandleShortLink(w, r):
return
case srv.ep.notFoundHandler != nil:
srv.ep.notFoundHandler.ServeHTTP(w, r)
default:
serveNotFound(w, r)
}
}
func (srv *httpServer) tryHandleShortLink(w http.ResponseWriter, r *http.Request) (handled bool) {
host := r.Host
if before, _, ok := strings.Cut(host, ":"); ok {
host = before
}
if strings.EqualFold(host, common.ShortLinkPrefix) {
if srv.ep.middleware != nil {
srv.ep.middleware.ServeHTTP(srv.ep.shortLinkMatcher.ServeHTTP, w, r)
} else {
srv.ep.shortLinkMatcher.ServeHTTP(w, r)
}
return true
}
return false
}
func serveNotFound(w http.ResponseWriter, r *http.Request) {
// Why use StatusNotFound instead of StatusBadRequest or StatusBadGateway?
// On nginx, when route for domain does not exist, it returns StatusBadGateway.
// Then scraper / scanners will know the subdomain is invalid.
// With StatusNotFound, they won't know whether it's the path, or the subdomain that is invalid.
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
log.Error().
Str("method", r.Method).
Str("url", r.URL.String()).
Str("remote", r.RemoteAddr).
Msgf("not found: %s", r.Host)
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
if ok {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
if _, err := w.Write(errorPage); err != nil {
log.Err(err).Msg("failed to write error page")
}
} else {
http.NotFound(w, r)
}
}
}

View File

@@ -0,0 +1,91 @@
package entrypoint
import (
"github.com/yusing/godoxy/internal/types"
)
// GetHealthInfo returns a map of route name to health info.
//
// The health info is for all routes, including excluded routes.
func (ep *Entrypoint) GetHealthInfo() map[string]types.HealthInfo {
healthMap := make(map[string]types.HealthInfo, ep.NumRoutes())
for r := range ep.IterRoutes {
healthMap[r.Name()] = getHealthInfo(r)
}
return healthMap
}
// GetHealthInfoWithoutDetail returns a map of route name to health info without detail.
//
// The health info is for all routes, including excluded routes.
func (ep *Entrypoint) GetHealthInfoWithoutDetail() map[string]types.HealthInfoWithoutDetail {
healthMap := make(map[string]types.HealthInfoWithoutDetail, ep.NumRoutes())
for r := range ep.IterRoutes {
healthMap[r.Name()] = getHealthInfoWithoutDetail(r)
}
return healthMap
}
// GetHealthInfoSimple returns a map of route name to health status.
//
// The health status is for all routes, including excluded routes.
func (ep *Entrypoint) GetHealthInfoSimple() map[string]types.HealthStatus {
healthMap := make(map[string]types.HealthStatus, ep.NumRoutes())
for r := range ep.IterRoutes {
healthMap[r.Name()] = getHealthInfoSimple(r)
}
return healthMap
}
// RoutesByProvider returns a map of provider name to routes.
//
// The routes are all routes, including excluded routes.
func (ep *Entrypoint) RoutesByProvider() map[string][]types.Route {
rts := make(map[string][]types.Route)
for r := range ep.IterRoutes {
rts[r.ProviderName()] = append(rts[r.ProviderName()], r)
}
return rts
}
func getHealthInfo(r types.Route) types.HealthInfo {
mon := r.HealthMonitor()
if mon == nil {
return types.HealthInfo{
HealthInfoWithoutDetail: types.HealthInfoWithoutDetail{
Status: types.StatusUnknown,
},
Detail: "n/a",
}
}
return types.HealthInfo{
HealthInfoWithoutDetail: types.HealthInfoWithoutDetail{
Status: mon.Status(),
Uptime: mon.Uptime(),
Latency: mon.Latency(),
},
Detail: mon.Detail(),
}
}
func getHealthInfoWithoutDetail(r types.Route) types.HealthInfoWithoutDetail {
mon := r.HealthMonitor()
if mon == nil {
return types.HealthInfoWithoutDetail{
Status: types.StatusUnknown,
}
}
return types.HealthInfoWithoutDetail{
Status: mon.Status(),
Uptime: mon.Uptime(),
Latency: mon.Latency(),
}
}
func getHealthInfoSimple(r types.Route) types.HealthStatus {
mon := r.HealthMonitor()
if mon == nil {
return types.StatusUnknown
}
return mon.Status()
}

View File

@@ -0,0 +1,145 @@
package entrypoint
import (
"errors"
"fmt"
"net"
"strconv"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/types"
)
func (ep *Entrypoint) IterRoutes(yield func(r types.Route) bool) {
for _, r := range ep.HTTPRoutes().Iter {
if !yield(r) {
return
}
}
for _, r := range ep.streamRoutes.Iter {
if !yield(r) {
return
}
}
for _, r := range ep.excludedRoutes.Iter {
if !yield(r) {
return
}
}
}
func (ep *Entrypoint) NumRoutes() int {
return ep.HTTPRoutes().Size() + ep.streamRoutes.Size() + ep.excludedRoutes.Size()
}
func (ep *Entrypoint) GetRoute(alias string) (types.Route, bool) {
if r, ok := ep.HTTPRoutes().Get(alias); ok {
return r, true
}
if r, ok := ep.streamRoutes.Get(alias); ok {
return r, true
}
if r, ok := ep.excludedRoutes.Get(alias); ok {
return r, true
}
return nil, false
}
func (ep *Entrypoint) StartAddRoute(r types.Route) error {
if r.ShouldExclude() {
ep.excludedRoutes.Add(r)
r.Task().OnCancel("remove_route", func() {
ep.excludedRoutes.Del(r)
})
return nil
}
switch r := r.(type) {
case types.HTTPRoute:
if err := ep.AddHTTPRoute(r); err != nil {
return err
}
ep.shortLinkMatcher.AddRoute(r.Key())
r.Task().OnCancel("remove_route", func() {
ep.delHTTPRoute(r)
ep.shortLinkMatcher.DelRoute(r.Key())
})
case types.StreamRoute:
err := r.ListenAndServe(r.Task().Context(), nil, nil)
if err != nil {
return err
}
ep.streamRoutes.Add(r)
r.Task().OnCancel("remove_route", func() {
r.Stream().Close()
ep.streamRoutes.Del(r)
})
default:
return fmt.Errorf("unknown route type: %T", r)
}
return nil
}
func getAddr(route types.HTTPRoute) (httpAddr, httpsAddr string) {
if port := route.ListenURL().Port(); port == "" || port == "0" {
host := route.ListenURL().Hostname()
if host == "" {
httpAddr = common.ProxyHTTPAddr
httpsAddr = common.ProxyHTTPSAddr
} else {
httpAddr = net.JoinHostPort(host, strconv.Itoa(common.ProxyHTTPPort))
httpsAddr = net.JoinHostPort(host, strconv.Itoa(common.ProxyHTTPSPort))
}
return httpAddr, httpsAddr
}
httpsAddr = route.ListenURL().Host
return
}
// AddHTTPRoute adds a HTTP route to the entrypoint's server.
//
// If the server does not exist, it will be created, started and return any error.
func (ep *Entrypoint) AddHTTPRoute(route types.HTTPRoute) error {
httpAddr, httpsAddr := getAddr(route)
var httpErr, httpsErr error
if httpAddr != "" {
httpErr = ep.addHTTPRoute(route, httpAddr, HTTPProtoHTTP)
}
if httpsAddr != "" {
httpsErr = ep.addHTTPRoute(route, httpsAddr, HTTPProtoHTTPS)
}
return errors.Join(httpErr, httpsErr)
}
func (ep *Entrypoint) addHTTPRoute(route types.HTTPRoute, addr string, proto HTTPProto) error {
var err error
srv, _ := ep.servers.LoadOrCompute(addr, func() (newSrv *httpServer, cancel bool) {
newSrv = newHTTPServer(ep)
err = newSrv.Listen(addr, proto)
cancel = err != nil
return
})
if err != nil {
return err
}
srv.AddRoute(route)
return nil
}
func (ep *Entrypoint) delHTTPRoute(route types.HTTPRoute) {
httpAddr, httpsAddr := getAddr(route)
if httpAddr != "" {
srv, _ := ep.servers.Load(httpAddr)
if srv != nil {
srv.DelRoute(route)
}
}
if httpsAddr != "" {
srv, _ := ep.servers.Load(httpsAddr)
if srv != nil {
srv.DelRoute(route)
}
}
}

View File

@@ -14,7 +14,7 @@ type ShortLinkMatcher struct {
subdomainRoutes *xsync.Map[string, struct{}]
}
func newShortLinkTree() *ShortLinkMatcher {
func newShortLinkMatcher() *ShortLinkMatcher {
return &ShortLinkMatcher{
fqdnRoutes: xsync.NewMap[string, string](),
subdomainRoutes: xsync.NewMap[string, struct{}](),

View File

@@ -6,13 +6,15 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yusing/godoxy/internal/common"
. "github.com/yusing/godoxy/internal/entrypoint"
"github.com/yusing/goutils/task"
)
func TestShortLinkMatcher_FQDNAlias(t *testing.T) {
ep := NewEntrypoint()
ep := NewEntrypoint(task.GetTestTask(t), nil)
matcher := ep.ShortLinkMatcher()
matcher.AddRoute("app.domain.com")
@@ -45,7 +47,7 @@ func TestShortLinkMatcher_FQDNAlias(t *testing.T) {
}
func TestShortLinkMatcher_SubdomainAlias(t *testing.T) {
ep := NewEntrypoint()
ep := NewEntrypoint(task.GetTestTask(t), nil)
matcher := ep.ShortLinkMatcher()
matcher.SetDefaultDomainSuffix(".example.com")
matcher.AddRoute("app")
@@ -70,7 +72,7 @@ func TestShortLinkMatcher_SubdomainAlias(t *testing.T) {
}
func TestShortLinkMatcher_NotFound(t *testing.T) {
ep := NewEntrypoint()
ep := NewEntrypoint(task.GetTestTask(t), nil)
matcher := ep.ShortLinkMatcher()
matcher.SetDefaultDomainSuffix(".example.com")
matcher.AddRoute("app")
@@ -93,7 +95,7 @@ func TestShortLinkMatcher_NotFound(t *testing.T) {
}
func TestShortLinkMatcher_AddDelRoute(t *testing.T) {
ep := NewEntrypoint()
ep := NewEntrypoint(task.GetTestTask(t), nil)
matcher := ep.ShortLinkMatcher()
matcher.SetDefaultDomainSuffix(".example.com")
@@ -131,7 +133,7 @@ func TestShortLinkMatcher_AddDelRoute(t *testing.T) {
}
func TestShortLinkMatcher_NoDefaultDomainSuffix(t *testing.T) {
ep := NewEntrypoint()
ep := NewEntrypoint(task.GetTestTask(t), nil)
matcher := ep.ShortLinkMatcher()
// no SetDefaultDomainSuffix called
@@ -158,15 +160,19 @@ func TestShortLinkMatcher_NoDefaultDomainSuffix(t *testing.T) {
}
func TestEntrypoint_ShortLinkDispatch(t *testing.T) {
ep := NewEntrypoint()
ep := NewEntrypoint(task.GetTestTask(t), nil)
ep.ShortLinkMatcher().SetDefaultDomainSuffix(".example.com")
ep.ShortLinkMatcher().AddRoute("app")
server := NewHTTPServer(ep)
err := server.Listen("localhost:0", HTTPProtoHTTP)
require.NoError(t, err)
t.Run("shortlink host", func(t *testing.T) {
req := httptest.NewRequest("GET", "/app", nil)
req.Host = common.ShortLinkPrefix
w := httptest.NewRecorder()
ep.ServeHTTP(w, req)
server.ServeHTTP(w, req)
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
assert.Equal(t, "https://app.example.com/", w.Header().Get("Location"))
@@ -176,7 +182,7 @@ func TestEntrypoint_ShortLinkDispatch(t *testing.T) {
req := httptest.NewRequest("GET", "/app", nil)
req.Host = common.ShortLinkPrefix + ":8080"
w := httptest.NewRecorder()
ep.ServeHTTP(w, req)
server.ServeHTTP(w, req)
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
assert.Equal(t, "https://app.example.com/", w.Header().Get("Location"))
@@ -186,7 +192,7 @@ func TestEntrypoint_ShortLinkDispatch(t *testing.T) {
req := httptest.NewRequest("GET", "/app", nil)
req.Host = "app.example.com"
w := httptest.NewRecorder()
ep.ServeHTTP(w, req)
server.ServeHTTP(w, req)
// Should not redirect, should try normal route lookup (which will 404)
assert.NotEqual(t, http.StatusTemporaryRedirect, w.Code)

View File

@@ -0,0 +1,18 @@
package entrypoint
import (
"context"
)
type ContextKey struct{}
func SetCtx(ctx interface{ SetValue(any, any) }, ep Entrypoint) {
ctx.SetValue(ContextKey{}, ep)
}
func FromCtx(ctx context.Context) Entrypoint {
if ep, ok := ctx.Value(ContextKey{}).(Entrypoint); ok {
return ep
}
return nil
}

View File

@@ -0,0 +1,37 @@
package entrypoint
import (
"github.com/yusing/godoxy/internal/types"
)
type Entrypoint interface {
SupportProxyProtocol() bool
DisablePoolsLog(v bool)
GetRoute(alias string) (types.Route, bool)
StartAddRoute(r types.Route) error
IterRoutes(yield func(r types.Route) bool)
NumRoutes() int
RoutesByProvider() map[string][]types.Route
HTTPRoutes() PoolLike[types.HTTPRoute]
StreamRoutes() PoolLike[types.StreamRoute]
ExcludedRoutes() RWPoolLike[types.Route]
GetHealthInfo() map[string]types.HealthInfo
GetHealthInfoWithoutDetail() map[string]types.HealthInfoWithoutDetail
GetHealthInfoSimple() map[string]types.HealthStatus
}
type PoolLike[Route types.Route] interface {
Get(alias string) (Route, bool)
Iter(yield func(alias string, r Route) bool)
Size() int
}
type RWPoolLike[Route types.Route] interface {
PoolLike[Route]
Add(r Route)
Del(r Route)
}

View File

@@ -41,7 +41,7 @@ type HealthCheckFunc func(url *url.URL) (result types.HealthCheckResult, err err
```go
type HealthMonitor interface {
Start(parent task.Parent) gperr.Error
Start(parent task.Parent) error
Task() *task.Task
Finish(reason any)
UpdateURL(url *url.URL)

View File

@@ -2,6 +2,7 @@ package monitor
import (
"context"
"errors"
"fmt"
"math/rand"
"net/url"
@@ -13,7 +14,6 @@ import (
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/synk"
"github.com/yusing/goutils/task"
@@ -42,7 +42,7 @@ type (
}
)
var ErrNegativeInterval = gperr.New("negative interval")
var ErrNegativeInterval = errors.New("negative interval")
func (mon *monitor) init(u *url.URL, cfg types.HealthCheckConfig, healthCheckFunc HealthCheckFunc) {
if state := config.WorkingState.Load(); state != nil {
@@ -79,7 +79,7 @@ func (mon *monitor) CheckHealth() (types.HealthCheckResult, error) {
}
// Start implements task.TaskStarter.
func (mon *monitor) Start(parent task.Parent) gperr.Error {
func (mon *monitor) Start(parent task.Parent) error {
if mon.config.Interval <= 0 {
return ErrNegativeInterval
}

View File

@@ -176,7 +176,7 @@ func (icon *Meta) Filenames(ref string) []string
func NewURL(source Source, refOrName, format string) *URL
// ErrInvalidIconURL is returned when icon URL parsing fails
var ErrInvalidIconURL = gperr.New("invalid icon url")
var ErrInvalidIconURL = errors.New("invalid icon url")
```
### Provider Interface

View File

@@ -1,6 +1,7 @@
package icons
import (
"errors"
"fmt"
"strings"
@@ -40,7 +41,7 @@ const (
VariantDark Variant = "dark"
)
var ErrInvalidIconURL = gperr.New("invalid icon url")
var ErrInvalidIconURL = errors.New("invalid icon url")
func NewURL(source Source, refOrName, format string) *URL {
switch source {
@@ -119,7 +120,7 @@ func (u *URL) parse(v string, checkExists bool) error {
case "@target", "": // @target/favicon.ico, /favicon.ico
url := v[slashIndex:]
if url == "/" {
return ErrInvalidIconURL.Withf("%s", "empty path")
return fmt.Errorf("%w: empty path", ErrInvalidIconURL)
}
u.FullURL = &url
u.Source = SourceRelative
@@ -131,7 +132,7 @@ func (u *URL) parse(v string, checkExists bool) error {
}
parts := strings.Split(v[slashIndex+1:], ".")
if len(parts) != 2 {
return ErrInvalidIconURL.Withf("expect @%s/<reference>.<format>, e.g. @%s/adguard-home.webp", beforeSlash, beforeSlash)
return fmt.Errorf("%w: expect %s/<reference>.<format>, e.g. %s/adguard-home.webp", ErrInvalidIconURL, beforeSlash, beforeSlash)
}
reference, format := parts[0], strings.ToLower(parts[1])
if reference == "" || format == "" {
@@ -140,7 +141,7 @@ func (u *URL) parse(v string, checkExists bool) error {
switch format {
case "svg", "png", "webp":
default:
return ErrInvalidIconURL.Withf("%s", "invalid image format, expect svg/png/webp")
return fmt.Errorf("%w: invalid image format, expect svg/png/webp", ErrInvalidIconURL)
}
isLight, isDark := false, false
if strings.HasSuffix(reference, "-light") {
@@ -158,10 +159,10 @@ func (u *URL) parse(v string, checkExists bool) error {
IsDark: isDark,
}
if checkExists && !u.HasIcon() {
return ErrInvalidIconURL.Withf("no such icon %s.%s from %s", reference, format, u.Source)
return fmt.Errorf("%w: no such icon %s.%s from %s", ErrInvalidIconURL, reference, format, u.Source)
}
default:
return ErrInvalidIconURL.Subject(v)
return gperr.PrependSubject(ErrInvalidIconURL, v)
}
return nil

View File

@@ -2,13 +2,13 @@ package qbittorrent
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"github.com/bytedance/sonic"
"github.com/yusing/godoxy/internal/homepage/widgets"
gperr "github.com/yusing/goutils/errs"
)
type Client struct {
@@ -46,7 +46,7 @@ func (c *Client) doRequest(ctx context.Context, method, endpoint string, query u
}
if resp.StatusCode != http.StatusOK {
return nil, gperr.Errorf("%w: %d %s", widgets.ErrHTTPStatus, resp.StatusCode, resp.Status)
return nil, fmt.Errorf("%w: %d %s", widgets.ErrHTTPStatus, resp.StatusCode, resp.Status)
}
return resp, nil

View File

@@ -50,8 +50,7 @@ const (
### Errors
```go
var ErrInvalidProvider = gperr.New("invalid provider")
var ErrHTTPStatus = gperr.New("http status")
var ErrInvalidProvider = errors.New("invalid provider")
```
## API Reference

View File

@@ -1,14 +1,13 @@
package widgets
import (
"errors"
"net/http"
"time"
gperr "github.com/yusing/goutils/errs"
)
var HTTPClient = &http.Client{
Timeout: 10 * time.Second,
}
var ErrHTTPStatus = gperr.New("http status")
var ErrHTTPStatus = errors.New("http status")

View File

@@ -2,6 +2,8 @@ package widgets
import (
"context"
"errors"
"fmt"
"github.com/yusing/godoxy/internal/serialization"
gperr "github.com/yusing/goutils/errs"
@@ -30,21 +32,21 @@ var widgetProviders = map[string]struct{}{
WidgetProviderQbittorrent: {},
}
var ErrInvalidProvider = gperr.New("invalid provider")
var ErrInvalidProvider = errors.New("invalid provider")
func (cfg *Config) UnmarshalMap(m map[string]any) error {
var ok bool
cfg.Provider, ok = m["provider"].(string)
if !ok {
return ErrInvalidProvider.Withf("non string")
return fmt.Errorf("%w: non string", ErrInvalidProvider)
}
if _, ok := widgetProviders[cfg.Provider]; !ok {
return ErrInvalidProvider.Subject(cfg.Provider)
return gperr.PrependSubject(ErrInvalidProvider, cfg.Provider)
}
delete(m, "provider")
m, ok = m["config"].(map[string]any)
if !ok {
return gperr.New("invalid config")
return errors.New("invalid config")
}
return serialization.MapUnmarshalValidate(m, &cfg.Config)
}

View File

@@ -70,7 +70,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
func (w *Watcher) Wake(ctx context.Context) error
// Start begins the idle watcher loop
func (w *Watcher) Start(parent task.Parent) gperr.Error
func (w *Watcher) Start(parent task.Parent) error
// ServeHTTP serves the loading page and SSE events
func (w *Watcher) ServeHTTP(rw http.ResponseWriter, r *http.Request)
@@ -103,7 +103,7 @@ var (
classDiagram
class Watcher {
+Wake(ctx) error
+Start(parent) gperr.Error
+Start(parent) error
+ServeHTTP(ResponseWriter, *Request)
+ListenAndServe(ctx, preDial, onRead)
+Key() string

View File

@@ -20,7 +20,7 @@ func (e *watcherError) Error() string {
}
func (w *Watcher) newWatcherError(err error) error {
if errors.Is(err, causeReload) {
if errors.Is(err, errCauseReload) {
return nil
}
if wErr, ok := err.(*watcherError); ok { //nolint:errorlint
@@ -44,7 +44,7 @@ func (e *depError) Error() string {
}
func (w *Watcher) newDepError(action string, dep *dependency, err error) error {
if errors.Is(err, causeReload) {
if errors.Is(err, errCauseReload) {
return nil
}
if dErr, ok := err.(*depError); ok { //nolint:errorlint

View File

@@ -7,10 +7,10 @@ import (
"net/http"
"strconv"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/homepage/icons"
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http"
_ "unsafe"
@@ -79,7 +79,7 @@ func (w *Watcher) handleWakeEventsSSE(rw http.ResponseWriter, r *http.Request) {
default:
err := errors.Join(event.WriteSSE(rw), controller.Flush())
if err != nil {
gperr.LogError("Failed to write SSE event", err, &w.l)
log.Err(err).Msg("Failed to write SSE event")
return
}
}
@@ -91,7 +91,7 @@ func (w *Watcher) handleWakeEventsSSE(rw http.ResponseWriter, r *http.Request) {
case event := <-eventCh:
err := errors.Join(event.WriteSSE(rw), controller.Flush())
if err != nil {
gperr.LogError("Failed to write SSE event", err, &w.l)
log.Err(err).Msg("Failed to write SSE event")
return
}
case <-ctx.Done():
@@ -169,7 +169,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
err := w.Wake(r.Context())
if err != nil {
gperr.LogError("Failed to wake container", err, &w.l)
log.Err(err).Msg("Failed to wake container")
if !acceptHTML {
http.Error(rw, "Failed to wake container", http.StatusInternalServerError)
return false

View File

@@ -10,8 +10,8 @@ import (
var _ nettypes.Stream = (*Watcher)(nil)
// ListenAndServe implements nettypes.Stream.
func (w *Watcher) ListenAndServe(ctx context.Context, predial, onRead nettypes.HookFunc) {
w.stream.ListenAndServe(ctx, func(ctx context.Context) error { //nolint:contextcheck
func (w *Watcher) ListenAndServe(ctx context.Context, predial, onRead nettypes.HookFunc) error {
return w.stream.ListenAndServe(ctx, func(ctx context.Context) error { //nolint:contextcheck
return w.preDial(ctx, predial)
}, func(ctx context.Context) error {
return w.onRead(ctx, onRead)

View File

@@ -1,16 +1,16 @@
package idlewatcher
import (
"fmt"
"time"
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/task"
)
// Start implements health.HealthMonitor.
func (w *Watcher) Start(parent task.Parent) gperr.Error {
func (w *Watcher) Start(parent task.Parent) error {
w.task.OnCancel("route_cleanup", func() {
parent.Finish(w.task.FinishCause())
})
@@ -113,7 +113,7 @@ func (w *Watcher) checkUpdateState() (ready bool, err error) {
if !state.startedAt.IsZero() {
elapsed := time.Since(state.startedAt)
if elapsed > w.cfg.WakeTimeout {
err := gperr.Errorf("container failed to become ready within %v (started at %v, %d health check attempts)",
err := fmt.Errorf("container failed to become ready within %v (started at %v, %d health check attempts)",
w.cfg.WakeTimeout, state.startedAt, state.healthTries)
w.l.Error().
Dur("elapsed", elapsed).

View File

@@ -2,6 +2,7 @@ package provider
import (
"context"
"fmt"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
@@ -9,7 +10,6 @@ import (
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/watcher"
gperr "github.com/yusing/goutils/errs"
)
type DockerProvider struct {
@@ -75,10 +75,10 @@ func (p *DockerProvider) ContainerStatus(ctx context.Context) (idlewatcher.Conta
case container.StatePaused:
return idlewatcher.ContainerStatusPaused, nil
}
return idlewatcher.ContainerStatusError, idlewatcher.ErrUnexpectedContainerStatus.Subject(string(status.Container.State.Status))
return idlewatcher.ContainerStatusError, fmt.Errorf("%w: %s", idlewatcher.ErrUnexpectedContainerStatus, status.Container.State.Status)
}
func (p *DockerProvider) Watch(ctx context.Context) (eventCh <-chan watcher.Event, errCh <-chan gperr.Error) {
func (p *DockerProvider) Watch(ctx context.Context) (eventCh <-chan watcher.Event, errCh <-chan error) {
return p.watcher.EventsWithOptions(ctx, watcher.DockerListOptions{
Filters: watcher.NewDockerFilters(
watcher.DockerFilterContainer,

View File

@@ -2,6 +2,8 @@ package provider
import (
"context"
"errors"
"fmt"
"strconv"
"time"
@@ -27,7 +29,7 @@ var ErrNodeNotFound = gperr.New("node not found in pool")
func NewProxmoxProvider(ctx context.Context, nodeName string, vmid int) (idlewatcher.Provider, error) {
if nodeName == "" || vmid == 0 {
return nil, gperr.New("node name and vmid are required")
return nil, errors.New("node name and vmid are required")
}
node, ok := proxmox.Nodes.Get(nodeName)
@@ -77,12 +79,12 @@ func (p *ProxmoxProvider) ContainerStatus(ctx context.Context) (idlewatcher.Cont
case proxmox.LXCStatusStopped:
return idlewatcher.ContainerStatusStopped, nil
}
return idlewatcher.ContainerStatusError, idlewatcher.ErrUnexpectedContainerStatus.Subject(string(status))
return idlewatcher.ContainerStatusError, fmt.Errorf("%w: %s", idlewatcher.ErrUnexpectedContainerStatus, string(status))
}
func (p *ProxmoxProvider) Watch(ctx context.Context) (<-chan watcher.Event, <-chan gperr.Error) {
func (p *ProxmoxProvider) Watch(ctx context.Context) (<-chan watcher.Event, <-chan error) {
eventCh := make(chan watcher.Event)
errCh := make(chan gperr.Error)
errCh := make(chan error)
go func() {
defer close(eventCh)
@@ -91,7 +93,7 @@ func (p *ProxmoxProvider) Watch(ctx context.Context) (<-chan watcher.Event, <-ch
var err error
p.running, err = p.LXCIsRunning(ctx, p.vmid)
if err != nil {
errCh <- gperr.Wrap(err)
errCh <- err
return
}
@@ -110,7 +112,7 @@ func (p *ProxmoxProvider) Watch(ctx context.Context) (<-chan watcher.Event, <-ch
case <-ticker.C:
status, err := p.ContainerStatus(ctx)
if err != nil {
errCh <- gperr.Wrap(err)
errCh <- err
return
}
running := status == idlewatcher.ContainerStatusRunning

View File

@@ -1,6 +1,6 @@
package idlewatcher
import gperr "github.com/yusing/goutils/errs"
import "errors"
type ContainerStatus string
@@ -11,4 +11,4 @@ const (
ContainerStatusStopped ContainerStatus = "stopped"
)
var ErrUnexpectedContainerStatus = gperr.New("unexpected container status")
var ErrUnexpectedContainerStatus = errors.New("unexpected container status")

View File

@@ -5,7 +5,6 @@ import (
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs"
)
type Provider interface {
@@ -15,6 +14,6 @@ type Provider interface {
ContainerStop(ctx context.Context, signal types.ContainerSignal, timeout int) error
ContainerKill(ctx context.Context, signal types.ContainerSignal) error
ContainerStatus(ctx context.Context) (ContainerStatus, error)
Watch(ctx context.Context) (eventCh <-chan events.Event, errCh <-chan gperr.Error)
Watch(ctx context.Context) (eventCh <-chan events.Event, errCh <-chan error)
Close()
}

View File

@@ -3,6 +3,7 @@ package idlewatcher
import (
"context"
"errors"
"fmt"
"math"
"strings"
"sync"
@@ -14,11 +15,11 @@ import (
"github.com/yusing/ds/ordered"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/docker"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
"github.com/yusing/godoxy/internal/health/monitor"
"github.com/yusing/godoxy/internal/idlewatcher/provider"
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
nettypes "github.com/yusing/godoxy/internal/net/types"
"github.com/yusing/godoxy/internal/route/routes"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs"
@@ -97,8 +98,8 @@ const (
)
var (
causeReload = gperr.New("reloaded") //nolint:errname
causeContainerDestroy = gperr.New("container destroyed") //nolint:errname
errCauseReload = errors.New("reloaded")
errCauseContainerDestroy = errors.New("container destroyed")
)
const reqTimeout = 3 * time.Second
@@ -173,7 +174,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
}
if !ok {
depRoute, ok = routes.GetIncludeExcluded(dep)
depRoute, ok = entrypoint.FromCtx(parent.Context()).GetRoute(dep)
if !ok {
depErrors.Addf("dependency %q not found", dep)
continue
@@ -286,7 +287,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
w.stream = r.Stream()
default:
p.Close()
return nil, w.newWatcherError(gperr.Errorf("unexpected route type: %T", r))
return nil, w.newWatcherError(fmt.Errorf("unexpected route type: %T", r))
}
w.route = r
@@ -320,12 +321,12 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
delete(watcherMap, key)
watcherMapMu.Unlock()
if errors.Is(cause, causeReload) {
if errors.Is(cause, errCauseReload) {
// no log
} else if errors.Is(cause, causeContainerDestroy) || errors.Is(cause, task.ErrProgramExiting) || errors.Is(cause, config.ErrConfigChanged) {
} else if errors.Is(cause, errCauseContainerDestroy) || errors.Is(cause, task.ErrProgramExiting) || errors.Is(cause, config.ErrConfigChanged) {
w.l.Info().Msg("idlewatcher stopped")
} else {
gperr.LogError("idlewatcher stopped unexpectedly", cause, &w.l)
w.l.Err(cause).Msg("idlewatcher stopped unexpectedly")
}
w.idleTicker.Stop()
@@ -467,7 +468,7 @@ func (w *Watcher) wakeIfStopped(ctx context.Context) error {
defer cancel()
p := w.provider.Load()
if p == nil {
return gperr.Errorf("provider not set")
return errors.New("provider not set")
}
switch state.status {
case idlewatcher.ContainerStatusStopped:
@@ -477,7 +478,7 @@ func (w *Watcher) wakeIfStopped(ctx context.Context) error {
w.sendEvent(WakeEventStarting, w.cfg.ContainerName()+" is unpausing...", nil)
return p.ContainerUnpause(ctx)
default:
return gperr.Errorf("unexpected container status: %s", state.status)
return fmt.Errorf("unexpected container status: %s", state.status)
}
}
@@ -512,7 +513,7 @@ func (w *Watcher) stopByMethod() error {
var err error
p := w.provider.Load()
if p == nil {
return gperr.New("provider not set")
return errors.New("provider not set")
}
switch cfg.StopMethod {
case types.ContainerStopMethodPause:
@@ -522,7 +523,7 @@ func (w *Watcher) stopByMethod() error {
case types.ContainerStopMethodKill:
err = p.ContainerKill(ctx, cfg.StopSignal)
default:
err = w.newWatcherError(gperr.Errorf("unexpected stop method: %q", cfg.StopMethod))
err = w.newWatcherError(fmt.Errorf("unexpected stop method: %q", cfg.StopMethod))
}
if err != nil {
@@ -564,7 +565,7 @@ func (w *Watcher) expires() time.Time {
func (w *Watcher) watchUntilDestroy() (returnCause error) {
p := w.provider.Load()
if p == nil {
return gperr.Errorf("provider not set")
return errors.New("provider not set")
}
defer p.Close()
eventCh, errCh := p.Watch(w.Task().Context())
@@ -572,14 +573,14 @@ func (w *Watcher) watchUntilDestroy() (returnCause error) {
for {
select {
case <-w.task.Context().Done():
return gperr.Wrap(w.task.FinishCause())
return w.task.FinishCause()
case err := <-errCh:
gperr.LogError("watcher error", err, &w.l)
w.l.Err(err).Msg("watcher error")
case e := <-eventCh:
w.l.Debug().Stringer("action", e.Action).Msg("state changed")
switch e.Action {
case events.ActionContainerDestroy:
return causeContainerDestroy
return errCauseContainerDestroy
case events.ActionForceReload:
continue
}

View File

@@ -1,11 +1,11 @@
package accesslog
import (
"errors"
"net/http"
"time"
"github.com/yusing/godoxy/internal/serialization"
gperr "github.com/yusing/goutils/errs"
)
type (
@@ -58,9 +58,9 @@ var (
ReqLoggerFormats = []Format{FormatCommon, FormatCombined, FormatJSON}
)
func (cfg *ConfigBase) Validate() gperr.Error {
func (cfg *ConfigBase) Validate() error {
if cfg.Path == "" && !cfg.Stdout {
return gperr.New("path or stdout is required")
return errors.New("path or stdout is required")
}
return nil
}

View File

@@ -1,6 +1,7 @@
package accesslog
import (
"fmt"
"io"
"net/http"
"sync"
@@ -11,7 +12,6 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
gperr "github.com/yusing/goutils/errs"
ioutils "github.com/yusing/goutils/io"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/synk"
@@ -161,9 +161,9 @@ func (l *fileAccessLogger) Rotate(result *RotateResult) (rotated bool, err error
func (l *fileAccessLogger) handleErr(err error) {
if l.errRateLimiter.Allow() {
gperr.LogError("failed to write access log", err, &l.logger)
l.logger.Err(err).Msg("failed to write access log")
} else {
gperr.LogError("too many errors, stopping access log", err, &l.logger)
l.logger.Err(err).Msg("too many errors, stopping access log")
l.task.Finish(err)
}
}
@@ -234,7 +234,7 @@ func (l *fileAccessLogger) write(data []byte) {
if err != nil {
l.handleErr(err)
} else if n < len(data) {
l.handleErr(gperr.Errorf("%w, writing %d bytes, only %d written", io.ErrShortWrite, len(data), n))
l.handleErr(fmt.Errorf("%w, writing %d bytes, only %d written", io.ErrShortWrite, len(data), n))
}
atomic.AddInt64(&l.writeCount, int64(n))
}

View File

@@ -1,12 +1,13 @@
package accesslog
import (
"errors"
"fmt"
"net"
"net/http"
"strings"
nettypes "github.com/yusing/godoxy/internal/net/types"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
@@ -29,7 +30,7 @@ type (
} // @name CIDR
)
var ErrInvalidHTTPHeaderFilter = gperr.New("invalid http header filter")
var ErrInvalidHTTPHeaderFilter = errors.New("invalid http header filter")
func (f *LogFilter[T]) CheckKeep(req *http.Request, res *http.Response) bool {
if len(f.Values) == 0 {
@@ -59,7 +60,7 @@ func (k *HTTPHeader) Parse(v string) error {
split = append(split, "")
case 2:
default:
return ErrInvalidHTTPHeaderFilter.Subject(v)
return fmt.Errorf("%w: %s", ErrInvalidHTTPHeaderFilter, v)
}
k.Key = split[0]
k.Value = split[1]

View File

@@ -1,10 +1,10 @@
package accesslog
import (
"errors"
"fmt"
"strconv"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
@@ -15,8 +15,8 @@ type Retention struct {
} // @name LogRetention
var (
ErrInvalidSyntax = gperr.New("invalid syntax")
ErrZeroValue = gperr.New("zero value")
ErrInvalidSyntax = errors.New("invalid syntax")
ErrZeroValue = errors.New("zero value")
)
// see back_scanner_test.go#L210 for benchmarks
@@ -34,7 +34,7 @@ var defaultChunkSize = 32 * kilobyte
func (r *Retention) Parse(v string) (err error) {
split := strutils.SplitSpace(v)
if len(split) != 2 {
return ErrInvalidSyntax.Subject(v)
return fmt.Errorf("%w: %s", ErrInvalidSyntax, v)
}
switch split[0] {
case "last":
@@ -64,7 +64,7 @@ func (r *Retention) Parse(v string) (err error) {
case "GB":
r.KeepSize = n * gigabyte
default:
return ErrInvalidSyntax.Subject("unit " + split[1])
return fmt.Errorf("%w: unit %s", ErrInvalidSyntax, split[1])
}
}
if !r.IsValid() {

View File

@@ -1,6 +1,8 @@
package accesslog
import (
"errors"
"fmt"
"strconv"
gperr "github.com/yusing/goutils/errs"
@@ -12,7 +14,7 @@ type StatusCodeRange struct {
End int
} // @name StatusCodeRange
var ErrInvalidStatusCodeRange = gperr.New("invalid status code range")
var ErrInvalidStatusCodeRange = errors.New("invalid status code range")
func (r *StatusCodeRange) Includes(code int) bool {
return r.Start <= code && code <= r.End
@@ -25,7 +27,7 @@ func (r *StatusCodeRange) Parse(v string) error {
case 1:
start, err := strconv.Atoi(split[0])
if err != nil {
return gperr.Wrap(err)
return err
}
r.Start = start
r.End = start
@@ -40,7 +42,7 @@ func (r *StatusCodeRange) Parse(v string) error {
r.End = end
return nil
default:
return ErrInvalidStatusCodeRange.Subject(v)
return fmt.Errorf("%w: %s", ErrInvalidStatusCodeRange, v)
}
}

View File

@@ -87,8 +87,28 @@ func multiWriter(out ...io.Writer) io.Writer {
func NewLogger(out ...io.Writer) zerolog.Logger {
writer := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
w.Out = diodeMultiWriter(out...)
if !common.IsTest {
w.Out = diodeMultiWriter(out...)
} else {
w.Out = multiWriter(out...)
}
w.TimeFormat = timeFmt
w.FormatPrepare = func(evt map[string]any) error {
// move error field to join message if it's multiline
if err, ok := evt[zerolog.ErrorFieldName].(string); ok {
if strings.Count(err, "\n") == 0 {
return nil
}
msg, ok := evt[zerolog.MessageFieldName].(string)
if ok && msg != "" {
evt[zerolog.MessageFieldName] = msg + "\n" + err
} else {
evt[zerolog.MessageFieldName] = err
}
delete(evt, zerolog.ErrorFieldName)
}
return nil
}
w.FormatMessage = func(msgI any) string { // pad spaces for each line
if msgI == nil {
return ""

View File

@@ -99,7 +99,7 @@ type Location struct {
```go
// LoadMaxMindDB loads or downloads the MaxMind database.
func (cfg *MaxMind) LoadMaxMindDB(parent task.Parent) gperr.Error
func (cfg *MaxMind) LoadMaxMindDB(parent task.Parent) error
```
### Lookup
@@ -324,8 +324,8 @@ The maxmind package integrates with:
```go
var (
ErrResponseNotOK = gperr.New("response not OK")
ErrDownloadFailure = gperr.New("download failure")
ErrResponseNotOK = errors.New("response not OK")
ErrDownloadFailure = errors.New("download failure")
)
```

View File

@@ -6,7 +6,6 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/notif"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/task"
)
@@ -24,7 +23,7 @@ func warnNotConfigured() {
})
}
func SetInstance(parent task.Parent, cfg *Config) gperr.Error {
func SetInstance(parent task.Parent, cfg *Config) error {
newInstance := &MaxMind{Config: cfg}
if err := newInstance.LoadMaxMindDB(parent); err != nil {
return err

View File

@@ -16,7 +16,6 @@ import (
"github.com/oschwald/maxminddb-golang"
"github.com/yusing/godoxy/internal/common"
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/task"
)
@@ -52,8 +51,8 @@ var httpClient = &http.Client{
}
var (
ErrResponseNotOK = gperr.New("response not OK")
ErrDownloadFailure = gperr.New("download failure")
ErrResponseNotOK = errors.New("response not OK")
ErrDownloadFailure = errors.New("download failure")
)
func (cfg *MaxMind) dbPath() string {
@@ -74,7 +73,7 @@ func (cfg *MaxMind) dbFilename() string {
return "GeoIP2-Country.mmdb"
}
func (cfg *MaxMind) LoadMaxMindDB(parent task.Parent) gperr.Error {
func (cfg *MaxMind) LoadMaxMindDB(parent task.Parent) error {
if cfg.Database == "" {
return nil
}
@@ -92,7 +91,7 @@ func (cfg *MaxMind) LoadMaxMindDB(parent task.Parent) gperr.Error {
// ignore invalid error, just download it again
var invalidErr maxminddb.InvalidDatabaseError
if !errors.As(err, &invalidErr) {
return gperr.Wrap(err)
return err
}
}
valid = false
@@ -101,7 +100,7 @@ func (cfg *MaxMind) LoadMaxMindDB(parent task.Parent) gperr.Error {
if !valid {
cfg.Logger().Info().Msg("MaxMind DB not found/invalid, downloading...")
if err = cfg.download(); err != nil {
return ErrDownloadFailure.With(err)
return fmt.Errorf("%w: %w", ErrDownloadFailure, err)
}
} else {
cfg.Logger().Info().Msg("MaxMind DB loaded")
@@ -236,7 +235,7 @@ func (cfg *MaxMind) download() error {
// extract .tar.gz and to database
err = extractFileFromTarGz(databaseGZ, cfg.dbFilename(), tmpDBPath)
if err != nil {
return gperr.New("failed to extract database from archive").With(err)
return err
}
// test if the downloaded database is valid

View File

@@ -3,7 +3,6 @@ package maxmind
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
)
@@ -21,7 +20,7 @@ const (
MaxMindGeoIP2 DatabaseType = "geoip2"
)
func (cfg *Config) Validate() gperr.Error {
func (cfg *Config) Validate() error {
if cfg.Database == "" {
cfg.Database = MaxMindGeoLite
}

View File

@@ -3,7 +3,6 @@ package period
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
@@ -153,8 +152,8 @@ func (p *Poller[T, AggregateT]) pollWithTimeout(ctx context.Context) {
p.lastResult.Store(data)
}
func (p *Poller[T, AggregateT]) Start() {
t := task.RootTask("poller."+p.name, true)
func (p *Poller[T, AggregateT]) Start(parent task.Parent) {
t := parent.Subtask("poller."+p.name, true)
l := log.With().Str("name", p.name).Logger()
err := p.load()
if err != nil {
@@ -196,7 +195,7 @@ func (p *Poller[T, AggregateT]) Start() {
if tickCount%gatherErrsTicks == 0 {
errs, ok := p.gatherErrs()
if ok {
gperr.LogError(fmt.Sprintf("poller %s has encountered %d errors in the last %s:", p.name, len(p.errs), gatherErrsInterval), errs)
log.Err(errs).Msgf("poller %s has encountered %d errors in the last %s:", p.name, len(p.errs), gatherErrsInterval)
}
p.clearErrs()
}

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