diff --git a/agent/cmd/main.go b/agent/cmd/main.go index 619f07ff..53fbf129 100644 --- a/agent/cmd/main.go +++ b/agent/cmd/main.go @@ -9,9 +9,9 @@ import ( "github.com/yusing/godoxy/agent/pkg/env" "github.com/yusing/godoxy/agent/pkg/server" "github.com/yusing/godoxy/internal/metrics/systeminfo" - httpServer "github.com/yusing/godoxy/internal/net/gphttp/server" "github.com/yusing/godoxy/pkg" socketproxy "github.com/yusing/godoxy/socketproxy/pkg" + httpServer "github.com/yusing/goutils/server" strutils "github.com/yusing/goutils/strings" "github.com/yusing/goutils/task" ) diff --git a/agent/go.mod b/agent/go.mod index ace5e532..95c38c2c 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -20,7 +20,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/yusing/godoxy v0.18.6 github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000 - github.com/yusing/goutils v0.3.1 + github.com/yusing/goutils v0.4.1 ) require ( @@ -69,7 +69,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/oschwald/maxminddb-golang v1.13.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -82,7 +81,6 @@ require ( github.com/samber/slog-zerolog/v2 v2.7.3 // indirect github.com/shirou/gopsutil/v4 v4.25.8 // indirect github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect - github.com/spf13/afero v1.15.0 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect @@ -107,7 +105,6 @@ require ( golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect - golang.org/x/time v0.13.0 // indirect golang.org/x/tools v0.37.0 // indirect google.golang.org/protobuf v1.36.9 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/agent/go.sum b/agent/go.sum index 3025ca58..aa397377 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -208,8 +208,8 @@ github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusing/ds v0.2.0 h1:lPhDU5eA2uvquVrBrzLCrQXRJJgSXlUYA53TbuK2sQY= github.com/yusing/ds v0.2.0/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk= -github.com/yusing/goutils v0.3.1 h1:xCPoZ/haI8ZJ0ZaPU4g6+okSPdBczs8o98tIZ/TbpsQ= -github.com/yusing/goutils v0.3.1/go.mod h1:meg9GcAU8yvBY21JgYjPuLsXD1Q5VdVHE32A4tG5Y5g= +github.com/yusing/goutils v0.4.1 h1:80uFNxXfm4zXMYDku0rWMLyqEiXO0UOMFOaUC4b/6fI= +github.com/yusing/goutils v0.4.1/go.mod h1:xsoLWLtIiu7k+9Bn6azERDs5o3Djb3b2/DW1htHrOjg= 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= diff --git a/agent/pkg/server/server.go b/agent/pkg/server/server.go index f8c92dfe..1fc0ae1b 100644 --- a/agent/pkg/server/server.go +++ b/agent/pkg/server/server.go @@ -9,7 +9,7 @@ import ( "github.com/rs/zerolog/log" "github.com/yusing/godoxy/agent/pkg/env" "github.com/yusing/godoxy/agent/pkg/handler" - "github.com/yusing/godoxy/internal/net/gphttp/server" + "github.com/yusing/goutils/server" "github.com/yusing/goutils/task" ) diff --git a/go.mod b/go.mod index 3a4f6504..4d5d42d7 100644 --- a/go.mod +++ b/go.mod @@ -42,15 +42,15 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/luthermonson/go-proxmox v0.2.3 github.com/oschwald/maxminddb-golang v1.13.1 - github.com/quic-go/quic-go v0.54.1 // http3 support - github.com/samber/slog-zerolog/v2 v2.7.3 + github.com/quic-go/quic-go v0.54.1 // indirect; http3 support + github.com/samber/slog-zerolog/v2 v2.7.3 // indirect github.com/spf13/afero v1.15.0 github.com/stretchr/testify v1.11.1 github.com/yusing/ds v0.2.0 github.com/yusing/godoxy/agent v0.0.0-20250926130035-55c1c918ba95 github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20250926130035-55c1c918ba95 github.com/yusing/godoxy/internal/utils v0.1.0 - github.com/yusing/goutils v0.3.1 + github.com/yusing/goutils v0.4.1 ) require ( diff --git a/go.sum b/go.sum index e598eaca..ab9cca54 100644 --- a/go.sum +++ b/go.sum @@ -1648,8 +1648,8 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusing/ds v0.2.0 h1:lPhDU5eA2uvquVrBrzLCrQXRJJgSXlUYA53TbuK2sQY= github.com/yusing/ds v0.2.0/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk= -github.com/yusing/goutils v0.3.1 h1:xCPoZ/haI8ZJ0ZaPU4g6+okSPdBczs8o98tIZ/TbpsQ= -github.com/yusing/goutils v0.3.1/go.mod h1:meg9GcAU8yvBY21JgYjPuLsXD1Q5VdVHE32A4tG5Y5g= +github.com/yusing/goutils v0.4.1 h1:80uFNxXfm4zXMYDku0rWMLyqEiXO0UOMFOaUC4b/6fI= +github.com/yusing/goutils v0.4.1/go.mod h1:xsoLWLtIiu7k+9Bn6azERDs5o3Djb3b2/DW1htHrOjg= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= diff --git a/internal/api/v1/agent/list.go b/internal/api/v1/agent/list.go index e616c378..bc93caff 100644 --- a/internal/api/v1/agent/list.go +++ b/internal/api/v1/agent/list.go @@ -6,8 +6,8 @@ import ( "github.com/gin-gonic/gin" "github.com/yusing/godoxy/agent/pkg/agent" - "github.com/yusing/godoxy/internal/net/gphttp/websocket" "github.com/yusing/goutils/http/httpheaders" + "github.com/yusing/goutils/http/websocket" ) // @x-id "list" diff --git a/internal/api/v1/cert/renew.go b/internal/api/v1/cert/renew.go index 51597e82..f2364b25 100644 --- a/internal/api/v1/cert/renew.go +++ b/internal/api/v1/cert/renew.go @@ -9,8 +9,8 @@ import ( apitypes "github.com/yusing/godoxy/internal/api/types" config "github.com/yusing/godoxy/internal/config/types" "github.com/yusing/godoxy/internal/logging/memlogger" - "github.com/yusing/godoxy/internal/net/gphttp/websocket" gperr "github.com/yusing/goutils/errs" + "github.com/yusing/goutils/http/websocket" ) // @x-id "renew" diff --git a/internal/api/v1/docker/logs.go b/internal/api/v1/docker/logs.go index 636bacdf..a871bf2e 100644 --- a/internal/api/v1/docker/logs.go +++ b/internal/api/v1/docker/logs.go @@ -12,7 +12,7 @@ import ( "github.com/rs/zerolog/log" apitypes "github.com/yusing/godoxy/internal/api/types" "github.com/yusing/godoxy/internal/docker" - "github.com/yusing/godoxy/internal/net/gphttp/websocket" + "github.com/yusing/goutils/http/websocket" "github.com/yusing/goutils/task" ) diff --git a/internal/api/v1/docker/utils.go b/internal/api/v1/docker/utils.go index 65b2b95c..b74d2195 100644 --- a/internal/api/v1/docker/utils.go +++ b/internal/api/v1/docker/utils.go @@ -10,9 +10,9 @@ import ( apitypes "github.com/yusing/godoxy/internal/api/types" config "github.com/yusing/godoxy/internal/config/types" "github.com/yusing/godoxy/internal/docker" - "github.com/yusing/godoxy/internal/net/gphttp/websocket" gperr "github.com/yusing/goutils/errs" "github.com/yusing/goutils/http/httpheaders" + "github.com/yusing/goutils/http/websocket" ) type ( diff --git a/internal/api/v1/health.go b/internal/api/v1/health.go index 453fe8de..ca83fe16 100644 --- a/internal/api/v1/health.go +++ b/internal/api/v1/health.go @@ -5,9 +5,9 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/yusing/godoxy/internal/net/gphttp/websocket" "github.com/yusing/godoxy/internal/route/routes" "github.com/yusing/goutils/http/httpheaders" + "github.com/yusing/goutils/http/websocket" ) type HealthMap = map[string]routes.HealthInfo // @name HealthMap diff --git a/internal/api/v1/homepage/items.go b/internal/api/v1/homepage/items.go index fc0dd011..87ca2c4d 100644 --- a/internal/api/v1/homepage/items.go +++ b/internal/api/v1/homepage/items.go @@ -12,9 +12,9 @@ import ( "github.com/lithammer/fuzzysearch/fuzzy" apitypes "github.com/yusing/godoxy/internal/api/types" "github.com/yusing/godoxy/internal/homepage" - "github.com/yusing/godoxy/internal/net/gphttp/websocket" "github.com/yusing/godoxy/internal/route/routes" "github.com/yusing/goutils/http/httpheaders" + "github.com/yusing/goutils/http/websocket" ) type HomepageItemsRequest struct { diff --git a/internal/api/v1/metrics/all_system_info.go b/internal/api/v1/metrics/all_system_info.go index f100ace1..cb4b864f 100644 --- a/internal/api/v1/metrics/all_system_info.go +++ b/internal/api/v1/metrics/all_system_info.go @@ -16,9 +16,9 @@ import ( apitypes "github.com/yusing/godoxy/internal/api/types" "github.com/yusing/godoxy/internal/metrics/period" "github.com/yusing/godoxy/internal/metrics/systeminfo" - "github.com/yusing/godoxy/internal/net/gphttp/websocket" gperr "github.com/yusing/goutils/errs" "github.com/yusing/goutils/http/httpheaders" + "github.com/yusing/goutils/http/websocket" "github.com/yusing/goutils/synk" ) diff --git a/internal/api/v1/route/providers.go b/internal/api/v1/route/providers.go index da454704..2f265469 100644 --- a/internal/api/v1/route/providers.go +++ b/internal/api/v1/route/providers.go @@ -6,8 +6,8 @@ import ( "github.com/gin-gonic/gin" config "github.com/yusing/godoxy/internal/config/types" - "github.com/yusing/godoxy/internal/net/gphttp/websocket" "github.com/yusing/goutils/http/httpheaders" + "github.com/yusing/goutils/http/websocket" ) // @x-id "providers" diff --git a/internal/api/v1/route/routes.go b/internal/api/v1/route/routes.go index ad77ea9b..b65a0297 100644 --- a/internal/api/v1/route/routes.go +++ b/internal/api/v1/route/routes.go @@ -6,11 +6,11 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/yusing/godoxy/internal/net/gphttp/websocket" "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" ) type RouteType route.Route // @name Route diff --git a/internal/api/v1/stats.go b/internal/api/v1/stats.go index c3b25867..380d6c2c 100644 --- a/internal/api/v1/stats.go +++ b/internal/api/v1/stats.go @@ -6,9 +6,9 @@ import ( "github.com/gin-gonic/gin" config "github.com/yusing/godoxy/internal/config/types" - "github.com/yusing/godoxy/internal/net/gphttp/websocket" "github.com/yusing/godoxy/internal/types" "github.com/yusing/goutils/http/httpheaders" + "github.com/yusing/goutils/http/websocket" ) type StatsResponse struct { diff --git a/internal/auth/oidc.go b/internal/auth/oidc.go index d5d5b347..acf49b7d 100644 --- a/internal/auth/oidc.go +++ b/internal/auth/oidc.go @@ -15,9 +15,9 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/rs/zerolog/log" "github.com/yusing/godoxy/internal/common" - "github.com/yusing/godoxy/internal/net/gphttp" "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" ) @@ -318,14 +318,14 @@ func (auth *OIDCProvider) PostAuthCallbackHandler(w http.ResponseWriter, r *http oauth2Token, err := auth.oauthConfig.Exchange(r.Context(), code, optRedirectPostAuth(r)) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) - gphttp.LogError(r).Msg(fmt.Sprintf("failed to exchange token: %v", err)) + httputils.LogError(r).Msg(fmt.Sprintf("failed to exchange token: %v", err)) return } idTokenJWT, idToken, err := auth.getIDToken(r.Context(), oauth2Token) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) - gphttp.LogError(r).Msg(fmt.Sprintf("failed to get ID token: %v", err)) + httputils.LogError(r).Msg(fmt.Sprintf("failed to get ID token: %v", err)) return } @@ -333,7 +333,7 @@ func (auth *OIDCProvider) PostAuthCallbackHandler(w http.ResponseWriter, r *http claims, err := parseClaims(idToken) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) - gphttp.LogError(r).Msg(fmt.Sprintf("failed to parse claims: %v", err)) + httputils.LogError(r).Msg(fmt.Sprintf("failed to parse claims: %v", err)) return } session := newSession(claims.Username, claims.Groups) diff --git a/internal/auth/userpass.go b/internal/auth/userpass.go index cab9ae54..7bf981a9 100644 --- a/internal/auth/userpass.go +++ b/internal/auth/userpass.go @@ -8,8 +8,8 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/yusing/godoxy/internal/common" - "github.com/yusing/godoxy/internal/net/gphttp" gperr "github.com/yusing/goutils/errs" + httputils "github.com/yusing/goutils/http" strutils "github.com/yusing/goutils/strings" "golang.org/x/crypto/bcrypt" ) @@ -122,7 +122,7 @@ func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http token, err := auth.NewToken() if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) - gphttp.LogError(r).Msg(fmt.Sprintf("failed to generate token: %v", err)) + httputils.LogError(r).Msg(fmt.Sprintf("failed to generate token: %v", err)) return } SetTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL) diff --git a/internal/common/env.go b/internal/common/env.go index 4463c1af..9da2b708 100644 --- a/internal/common/env.go +++ b/internal/common/env.go @@ -17,8 +17,6 @@ var ( IsDebug = env.GetEnvBool("DEBUG", IsTest) IsTrace = env.GetEnvBool("TRACE", false) && IsDebug - HTTP3Enabled = env.GetEnvBool("HTTP3_ENABLED", true) - ProxyHTTPAddr, ProxyHTTPHost, ProxyHTTPPort, diff --git a/internal/config/config.go b/internal/config/config.go index ef18b02a..a787da85 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,7 +19,6 @@ import ( config "github.com/yusing/godoxy/internal/config/types" "github.com/yusing/godoxy/internal/entrypoint" "github.com/yusing/godoxy/internal/maxmind" - "github.com/yusing/godoxy/internal/net/gphttp/server" "github.com/yusing/godoxy/internal/notif" "github.com/yusing/godoxy/internal/proxmox" proxy "github.com/yusing/godoxy/internal/route/provider" @@ -27,6 +26,7 @@ import ( "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" ) diff --git a/internal/dnsproviders/go.mod b/internal/dnsproviders/go.mod index fb9a9206..880454c2 100644 --- a/internal/dnsproviders/go.mod +++ b/internal/dnsproviders/go.mod @@ -148,7 +148,7 @@ require ( github.com/vultr/govultr/v3 v3.24.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yusing/godoxy/internal/utils v0.1.0 // indirect - github.com/yusing/goutils v0.3.1 // indirect + github.com/yusing/goutils v0.4.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect diff --git a/internal/dnsproviders/go.sum b/internal/dnsproviders/go.sum index 673e76bc..ee9398f1 100644 --- a/internal/dnsproviders/go.sum +++ b/internal/dnsproviders/go.sum @@ -1513,8 +1513,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yusing/goutils v0.3.1 h1:xCPoZ/haI8ZJ0ZaPU4g6+okSPdBczs8o98tIZ/TbpsQ= -github.com/yusing/goutils v0.3.1/go.mod h1:meg9GcAU8yvBY21JgYjPuLsXD1Q5VdVHE32A4tG5Y5g= +github.com/yusing/goutils v0.4.1 h1:80uFNxXfm4zXMYDku0rWMLyqEiXO0UOMFOaUC4b/6fI= +github.com/yusing/goutils v0.4.1/go.mod h1:xsoLWLtIiu7k+9Bn6azERDs5o3Djb3b2/DW1htHrOjg= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= diff --git a/internal/homepage/favicon.go b/internal/homepage/favicon.go index af7d8385..0a0e0b13 100644 --- a/internal/homepage/favicon.go +++ b/internal/homepage/favicon.go @@ -14,6 +14,7 @@ import ( "github.com/PuerkitoBio/goquery" "github.com/vincent-petithory/dataurl" gphttp "github.com/yusing/godoxy/internal/net/gphttp" + httputils "github.com/yusing/goutils/http" strutils "github.com/yusing/goutils/strings" ) @@ -196,7 +197,7 @@ func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string) return &FetchResult{StatusCode: c.status, ErrMsg: "upstream error: " + string(c.data)} } // return icon data - if !gphttp.GetContentType(c.header).IsHTML() { + if !httputils.GetContentType(c.header).IsHTML() { return &FetchResult{Icon: c.data, contentType: c.header.Get("Content-Type")} } // try extract from "link[rel=icon]" from path "/" diff --git a/internal/idlewatcher/handle_http.go b/internal/idlewatcher/handle_http.go index d3213ca3..9d7dac4b 100644 --- a/internal/idlewatcher/handle_http.go +++ b/internal/idlewatcher/handle_http.go @@ -6,7 +6,7 @@ import ( "strconv" api "github.com/yusing/godoxy/internal/api/v1" - gphttp "github.com/yusing/godoxy/internal/net/gphttp" + httputils "github.com/yusing/goutils/http" "github.com/yusing/goutils/http/httpheaders" ) @@ -79,7 +79,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN return false } - accept := gphttp.GetAccept(r.Header) + accept := httputils.GetAccept(r.Header) acceptHTML := (r.Method == http.MethodGet && accept.AcceptHTML() || r.RequestURI == "/" && accept.IsEmpty()) isCheckRedirect := r.Header.Get(httpheaders.HeaderGoDoxyCheckRedirect) != "" @@ -108,7 +108,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN err := w.Wake(ctx) if err != nil { http.Error(rw, "Internal Server Error", http.StatusInternalServerError) - gphttp.LogError(r).Msg(fmt.Sprintf("failed to wake: %v", err)) + httputils.LogError(r).Msg(fmt.Sprintf("failed to wake: %v", err)) return false } diff --git a/internal/logging/accesslog/rotate_test.go b/internal/logging/accesslog/rotate_test.go index e74e6567..7d049d13 100644 --- a/internal/logging/accesslog/rotate_test.go +++ b/internal/logging/accesslog/rotate_test.go @@ -8,9 +8,9 @@ import ( . "github.com/yusing/godoxy/internal/logging/accesslog" "github.com/yusing/godoxy/internal/utils" - expect "github.com/yusing/goutils/testing" strutils "github.com/yusing/goutils/strings" "github.com/yusing/goutils/task" + expect "github.com/yusing/goutils/testing" ) var ( diff --git a/internal/logging/memlogger/mem_logger.go b/internal/logging/memlogger/mem_logger.go index 77f2d04d..67874a20 100644 --- a/internal/logging/memlogger/mem_logger.go +++ b/internal/logging/memlogger/mem_logger.go @@ -10,7 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/puzpuzpuz/xsync/v4" apitypes "github.com/yusing/godoxy/internal/api/types" - "github.com/yusing/godoxy/internal/net/gphttp/websocket" + "github.com/yusing/goutils/http/websocket" ) type logEntryRange struct { diff --git a/internal/metrics/period/handler.go b/internal/metrics/period/handler.go index 5cf0212e..a911138d 100644 --- a/internal/metrics/period/handler.go +++ b/internal/metrics/period/handler.go @@ -8,8 +8,8 @@ import ( "github.com/gin-gonic/gin" apitypes "github.com/yusing/godoxy/internal/api/types" metricsutils "github.com/yusing/godoxy/internal/metrics/utils" - "github.com/yusing/godoxy/internal/net/gphttp/websocket" "github.com/yusing/goutils/http/httpheaders" + "github.com/yusing/goutils/http/websocket" ) type ResponseType[AggregateT any] struct { diff --git a/internal/net/gphttp/body.go b/internal/net/gphttp/body.go deleted file mode 100644 index be0bf51b..00000000 --- a/internal/net/gphttp/body.go +++ /dev/null @@ -1,48 +0,0 @@ -package gphttp - -import ( - "context" - "encoding/json" - "errors" - "net/http" - - "github.com/rs/zerolog/log" -) - -func WriteBody(w http.ResponseWriter, body []byte) { - if _, err := w.Write(body); err != nil { - switch { - case errors.Is(err, http.ErrHandlerTimeout), - errors.Is(err, context.DeadlineExceeded): - log.Err(err).Msg("timeout writing body") - default: - log.Err(err).Msg("failed to write body") - } - } -} - -func RespondJSON(w http.ResponseWriter, r *http.Request, data any, code ...int) (canProceed bool) { - if data == nil { - http.NotFound(w, r) - return false - } - - if len(code) > 0 { - w.WriteHeader(code[0]) - } - w.Header().Set("Content-Type", "application/json") - var err error - - switch data := data.(type) { - case []byte: - panic("use WriteBody instead") - default: - err = json.NewEncoder(w).Encode(data) - } - - if err != nil { - LogError(r).Err(err).Msg("failed to encode json") - return false - } - return true -} diff --git a/internal/net/gphttp/content_type.go b/internal/net/gphttp/content_type.go deleted file mode 100644 index 7dd6bbb2..00000000 --- a/internal/net/gphttp/content_type.go +++ /dev/null @@ -1,104 +0,0 @@ -package gphttp - -import ( - "mime" - "net/http" - "strings" -) - -type ( - ContentType string - AcceptContentType []ContentType -) - -const ( - ContentTypeJSON = ContentType("application/json") - ContentTypeTextPlain = ContentType("text/plain") - ContentTypeTextHTML = ContentType("text/html") - ContentTypeTextMarkdown = ContentType("text/markdown") - ContentTypeTextXML = ContentType("text/xml") - ContentTypeXHTML = ContentType("application/xhtml+xml") -) - -func GetContentType(h http.Header) ContentType { - ct := h.Get("Content-Type") - if ct == "" { - return "" - } - ct, _, err := mime.ParseMediaType(ct) - if err != nil { - return "" - } - return ContentType(ct) -} - -func GetAccept(h http.Header) AcceptContentType { - var accepts []ContentType - acceptHeader := h["Accept"] - if len(acceptHeader) == 1 { - acceptHeader = strings.Split(acceptHeader[0], ",") - } - for _, v := range acceptHeader { - ct, _, err := mime.ParseMediaType(v) - if err != nil { - continue - } - accepts = append(accepts, ContentType(ct)) - } - if len(accepts) == 0 { - return []ContentType{"*/*"} - } - return accepts -} - -func (ct ContentType) IsHTML() bool { - return ct == ContentTypeTextHTML || ct == ContentTypeXHTML -} - -func (ct ContentType) IsJSON() bool { - return ct == ContentTypeJSON -} - -func (ct ContentType) IsPlainText() bool { - return ct == ContentTypeTextPlain -} - -func (act AcceptContentType) IsEmpty() bool { - return len(act) == 0 -} - -func (act AcceptContentType) AcceptHTML() bool { - for _, v := range act { - if v.IsHTML() || v == "text/*" || v == "*/*" { - return true - } - } - return false -} - -func (act AcceptContentType) AcceptJSON() bool { - for _, v := range act { - if v.IsJSON() || v == "*/*" { - return true - } - } - return false -} - -func (act AcceptContentType) AcceptMarkdown() bool { - for _, v := range act { - if v == ContentTypeTextMarkdown || v == "*/*" { - return true - } - } - return false -} - -func (act AcceptContentType) AcceptPlainText() bool { - for _, v := range act { - if v.IsPlainText() || v == "text/*" || v == "*/*" { - return true - } - } - return false -} diff --git a/internal/net/gphttp/content_type_test.go b/internal/net/gphttp/content_type_test.go deleted file mode 100644 index aa167ea2..00000000 --- a/internal/net/gphttp/content_type_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package gphttp - -import ( - "net/http" - "testing" - - expect "github.com/yusing/goutils/testing" -) - -func TestContentTypes(t *testing.T) { - expect.True(t, GetContentType(http.Header{"Content-Type": {"text/html"}}).IsHTML()) - expect.True(t, GetContentType(http.Header{"Content-Type": {"text/html; charset=utf-8"}}).IsHTML()) - expect.True(t, GetContentType(http.Header{"Content-Type": {"application/xhtml+xml"}}).IsHTML()) - expect.False(t, GetContentType(http.Header{"Content-Type": {"text/plain"}}).IsHTML()) - - expect.True(t, GetContentType(http.Header{"Content-Type": {"application/json"}}).IsJSON()) - expect.True(t, GetContentType(http.Header{"Content-Type": {"application/json; charset=utf-8"}}).IsJSON()) - expect.False(t, GetContentType(http.Header{"Content-Type": {"text/html"}}).IsJSON()) - - expect.True(t, GetContentType(http.Header{"Content-Type": {"text/plain"}}).IsPlainText()) - expect.True(t, GetContentType(http.Header{"Content-Type": {"text/plain; charset=utf-8"}}).IsPlainText()) - expect.False(t, GetContentType(http.Header{"Content-Type": {"text/html"}}).IsPlainText()) -} - -func TestAcceptContentTypes(t *testing.T) { - expect.True(t, GetAccept(http.Header{"Accept": {"text/html", "text/plain"}}).AcceptPlainText()) - expect.True(t, GetAccept(http.Header{"Accept": {"text/html", "text/plain; charset=utf-8"}}).AcceptPlainText()) - expect.True(t, GetAccept(http.Header{"Accept": {"text/html", "text/plain"}}).AcceptHTML()) - expect.True(t, GetAccept(http.Header{"Accept": {"application/json"}}).AcceptJSON()) - expect.True(t, GetAccept(http.Header{"Accept": {"*/*"}}).AcceptPlainText()) - expect.True(t, GetAccept(http.Header{"Accept": {"*/*"}}).AcceptHTML()) - expect.True(t, GetAccept(http.Header{"Accept": {"*/*"}}).AcceptJSON()) - expect.True(t, GetAccept(http.Header{"Accept": {"text/*"}}).AcceptPlainText()) - expect.True(t, GetAccept(http.Header{"Accept": {"text/*"}}).AcceptHTML()) - - expect.False(t, GetAccept(http.Header{"Accept": {"text/plain"}}).AcceptHTML()) - expect.False(t, GetAccept(http.Header{"Accept": {"text/plain; charset=utf-8"}}).AcceptHTML()) - expect.False(t, GetAccept(http.Header{"Accept": {"text/html"}}).AcceptPlainText()) - expect.False(t, GetAccept(http.Header{"Accept": {"text/html"}}).AcceptJSON()) - expect.False(t, GetAccept(http.Header{"Accept": {"text/*"}}).AcceptJSON()) -} diff --git a/internal/net/gphttp/logging.go b/internal/net/gphttp/logging.go deleted file mode 100644 index 9db2f35e..00000000 --- a/internal/net/gphttp/logging.go +++ /dev/null @@ -1,20 +0,0 @@ -package gphttp - -import ( - "net/http" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" -) - -func reqLogger(r *http.Request, level zerolog.Level) *zerolog.Event { - return log.WithLevel(level). //nolint:zerologlint - Str("remote", r.RemoteAddr). - Str("host", r.Host). - Str("uri", r.Method+" "+r.RequestURI) -} - -func LogError(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.ErrorLevel) } -func LogWarn(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.WarnLevel) } -func LogInfo(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.InfoLevel) } -func LogDebug(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.DebugLevel) } diff --git a/internal/net/gphttp/methods.go b/internal/net/gphttp/methods.go deleted file mode 100644 index 6e49202b..00000000 --- a/internal/net/gphttp/methods.go +++ /dev/null @@ -1,20 +0,0 @@ -package gphttp - -import "net/http" - -func IsMethodValid(method string) bool { - switch method { - case http.MethodGet, - http.MethodHead, - http.MethodPost, - http.MethodPut, - http.MethodPatch, - http.MethodDelete, - http.MethodConnect, - http.MethodOptions, - http.MethodTrace: - return true - default: - return false - } -} diff --git a/internal/net/gphttp/middleware/captcha/middleware.go b/internal/net/gphttp/middleware/captcha/middleware.go index 604e21c8..db44ac59 100644 --- a/internal/net/gphttp/middleware/captcha/middleware.go +++ b/internal/net/gphttp/middleware/captcha/middleware.go @@ -6,7 +6,7 @@ import ( "github.com/rs/zerolog/log" "github.com/yusing/godoxy/internal/auth" - "github.com/yusing/godoxy/internal/net/gphttp" + httputils "github.com/yusing/goutils/http" _ "embed" ) @@ -31,7 +31,7 @@ func PreRequest(p Provider, w http.ResponseWriter, r *http.Request) (proceed boo } } - if !gphttp.GetAccept(r.Header).AcceptHTML() { + if !httputils.GetAccept(r.Header).AcceptHTML() { http.Error(w, "Captcha is required", http.StatusForbidden) return false } diff --git a/internal/net/gphttp/middleware/cidr_whitelist.go b/internal/net/gphttp/middleware/cidr_whitelist.go index dc6e63ba..fff9d611 100644 --- a/internal/net/gphttp/middleware/cidr_whitelist.go +++ b/internal/net/gphttp/middleware/cidr_whitelist.go @@ -6,9 +6,9 @@ import ( "github.com/go-playground/validator/v10" "github.com/puzpuzpuz/xsync/v4" - gphttp "github.com/yusing/godoxy/internal/net/gphttp" nettypes "github.com/yusing/godoxy/internal/net/types" "github.com/yusing/godoxy/internal/serialization" + httputils "github.com/yusing/goutils/http" ) type ( @@ -35,7 +35,7 @@ var ( func init() { serialization.MustRegisterValidation("status_code", func(fl validator.FieldLevel) bool { statusCode := fl.Field().Int() - return gphttp.IsStatusCodeValid(int(statusCode)) + return httputils.IsStatusCodeValid(int(statusCode)) }) } diff --git a/internal/net/gphttp/middleware/custom_error_page.go b/internal/net/gphttp/middleware/custom_error_page.go index 06a7bd81..6c4e009e 100644 --- a/internal/net/gphttp/middleware/custom_error_page.go +++ b/internal/net/gphttp/middleware/custom_error_page.go @@ -9,8 +9,8 @@ import ( "strings" "github.com/rs/zerolog/log" - gphttp "github.com/yusing/godoxy/internal/net/gphttp" "github.com/yusing/godoxy/internal/net/gphttp/middleware/errorpage" + httputils "github.com/yusing/goutils/http" "github.com/yusing/goutils/http/httpheaders" ) @@ -28,8 +28,8 @@ func (customErrorPage) before(w http.ResponseWriter, r *http.Request) (proceed b // modifyResponse implements ResponseModifier. func (customErrorPage) modifyResponse(resp *http.Response) error { // only handles non-success status code and html/plain content type - contentType := gphttp.GetContentType(resp.Header) - if !gphttp.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) { + contentType := httputils.GetContentType(resp.Header) + if !httputils.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) { errorPage, ok := errorpage.GetErrorPageByStatus(resp.StatusCode) if ok { log.Debug().Msgf("error page for status %d loaded", resp.StatusCode) diff --git a/internal/net/gphttp/middleware/middleware.go b/internal/net/gphttp/middleware/middleware.go index fefcac7f..67c1e038 100644 --- a/internal/net/gphttp/middleware/middleware.go +++ b/internal/net/gphttp/middleware/middleware.go @@ -10,9 +10,9 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" - gphttp "github.com/yusing/godoxy/internal/net/gphttp" "github.com/yusing/godoxy/internal/serialization" gperr "github.com/yusing/goutils/errs" + httputils "github.com/yusing/goutils/http" "github.com/yusing/goutils/http/reverseproxy" ) @@ -179,7 +179,7 @@ func (m *Middleware) ModifyResponse(resp *http.Response) error { func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r *http.Request) { if exec, ok := m.impl.(ResponseModifier); ok { - w = gphttp.NewModifyResponseWriter(w, r, func(resp *http.Response) error { + w = httputils.NewModifyResponseWriter(w, r, func(resp *http.Response) error { return exec.modifyResponse(resp) }) } diff --git a/internal/net/gphttp/middleware/modify_html.go b/internal/net/gphttp/middleware/modify_html.go index 32b77ffd..ec10e099 100644 --- a/internal/net/gphttp/middleware/modify_html.go +++ b/internal/net/gphttp/middleware/modify_html.go @@ -8,7 +8,6 @@ import ( "github.com/PuerkitoBio/goquery" "github.com/rs/zerolog/log" - gphttp "github.com/yusing/godoxy/internal/net/gphttp" httputils "github.com/yusing/goutils/http" ioutils "github.com/yusing/goutils/io" "github.com/yusing/goutils/synk" @@ -36,7 +35,7 @@ func (m *modifyHTML) before(_ http.ResponseWriter, req *http.Request) bool { // modifyResponse implements ResponseModifier. func (m *modifyHTML) modifyResponse(resp *http.Response) error { // including text/html and application/xhtml+xml - if !gphttp.GetContentType(resp.Header).IsHTML() { + if !httputils.GetContentType(resp.Header).IsHTML() { return nil } diff --git a/internal/net/gphttp/modify_response_writer.go b/internal/net/gphttp/modify_response_writer.go deleted file mode 100644 index 41f846dd..00000000 --- a/internal/net/gphttp/modify_response_writer.go +++ /dev/null @@ -1,116 +0,0 @@ -// Modified from Traefik Labs's MIT-licensed code (https://github.com/traefik/traefik/blob/master/pkg/middlewares/response_modifier.go) -// Copyright (c) 2020-2024 Traefik Labs - -package gphttp - -import ( - "bufio" - "fmt" - "net" - "net/http" -) - -type ( - ModifyResponseFunc func(*http.Response) error - ModifyResponseWriter struct { - w http.ResponseWriter - r *http.Request - - headerSent bool - code int - size int - - modifier ModifyResponseFunc - modified bool - modifierErr error - } -) - -func NewModifyResponseWriter(w http.ResponseWriter, r *http.Request, f ModifyResponseFunc) *ModifyResponseWriter { - return &ModifyResponseWriter{ - w: w, - r: r, - modifier: f, - code: http.StatusOK, - } -} - -func (w *ModifyResponseWriter) Unwrap() http.ResponseWriter { - return w.w -} - -func (w *ModifyResponseWriter) StatusCode() int { - return w.code -} - -func (w *ModifyResponseWriter) Size() int { - return w.size -} - -func (w *ModifyResponseWriter) WriteHeader(code int) { - if w.headerSent { - return - } - - if code >= http.StatusContinue && code < http.StatusOK { - w.w.WriteHeader(code) - } - - defer func() { - w.headerSent = true - w.code = code - }() - - if w.modifier == nil || w.modified { - w.w.WriteHeader(code) - return - } - - resp := http.Response{ - StatusCode: code, - Header: w.w.Header(), - Request: w.r, - ContentLength: int64(w.size), - } - - if err := w.modifier(&resp); err != nil { - w.modifierErr = fmt.Errorf("response modifier error: %w", err) - resp.Status = w.modifierErr.Error() - w.w.WriteHeader(http.StatusInternalServerError) - return - } - - w.modified = true - w.w.WriteHeader(code) -} - -func (w *ModifyResponseWriter) Header() http.Header { - return w.w.Header() -} - -func (w *ModifyResponseWriter) Write(b []byte) (int, error) { - w.WriteHeader(w.code) - if w.modifierErr != nil { - return 0, w.modifierErr - } - - n, err := w.w.Write(b) - w.size += n - return n, err -} - -// Hijack hijacks the connection. -func (w *ModifyResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - if h, ok := w.w.(http.Hijacker); ok { - return h.Hijack() - } - - return nil, nil, fmt.Errorf("not a hijacker: %T", w.w) -} - -// Flush sends any buffered data to the client. -func (w *ModifyResponseWriter) Flush() { - if flusher, ok := w.w.(http.Flusher); ok { - flusher.Flush() - } -} diff --git a/internal/net/gphttp/server/error.go b/internal/net/gphttp/server/error.go deleted file mode 100644 index 5fd8d5c9..00000000 --- a/internal/net/gphttp/server/error.go +++ /dev/null @@ -1,23 +0,0 @@ -package server - -import ( - "context" - "errors" - "net" - "net/http" - - "github.com/rs/zerolog" -) - -func convertError(err error) error { - switch { - case err == nil, errors.Is(err, http.ErrServerClosed), errors.Is(err, context.Canceled), errors.Is(err, net.ErrClosed): - return nil - default: - return err - } -} - -func HandleError(logger *zerolog.Logger, err error, msg string) { - logger.Fatal().Err(err).Msg(msg) -} diff --git a/internal/net/gphttp/server/server.go b/internal/net/gphttp/server/server.go deleted file mode 100644 index fb0d15cf..00000000 --- a/internal/net/gphttp/server/server.go +++ /dev/null @@ -1,268 +0,0 @@ -package server - -import ( - "context" - "crypto/tls" - "errors" - "io" - "net" - "net/http" - "time" - - "github.com/pires/go-proxyproto" - h2proxy "github.com/pires/go-proxyproto/helper/http2" - "github.com/quic-go/quic-go/http3" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/yusing/godoxy/internal/acl" - "github.com/yusing/godoxy/internal/common" - "github.com/yusing/goutils/task" -) - -type CertProvider interface { - GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) -} - -type Server struct { - Name string - CertProvider CertProvider - http *http.Server - https *http.Server - startTime time.Time - acl *acl.Config - proxyProto bool - - l zerolog.Logger -} - -type Options struct { - Name string - HTTPAddr string - HTTPSAddr string - CertProvider CertProvider - Handler http.Handler - ACL *acl.Config - - SupportProxyProtocol bool -} - -type httpServer interface { - *http.Server | *http3.Server - Shutdown(ctx context.Context) error -} - -func StartServer(parent task.Parent, opt Options) (s *Server) { - s = NewServer(opt) - s.Start(parent) - return s -} - -func NewServer(opt Options) (s *Server) { - var httpSer, httpsSer *http.Server - - logger := log.With().Str("server", opt.Name).Logger() - - certAvailable := false - if opt.CertProvider != nil { - _, err := opt.CertProvider.GetCert(nil) - certAvailable = err == nil - } - - if opt.HTTPAddr != "" { - httpSer = &http.Server{ - Addr: opt.HTTPAddr, - Handler: opt.Handler, - } - } - if certAvailable && opt.HTTPSAddr != "" { - httpsSer = &http.Server{ - Addr: opt.HTTPSAddr, - Handler: opt.Handler, - TLSConfig: &tls.Config{ - GetCertificate: opt.CertProvider.GetCert, - MinVersion: tls.VersionTLS12, - }, - } - } - return &Server{ - Name: opt.Name, - CertProvider: opt.CertProvider, - http: httpSer, - https: httpsSer, - l: logger, - acl: opt.ACL, - proxyProto: opt.SupportProxyProtocol, - } -} - -// Start will start the http and https servers. -// -// If both are not set, this does nothing. -// -// Start() is non-blocking. -func (s *Server) Start(parent task.Parent) { - s.startTime = time.Now() - subtask := parent.Subtask("server."+s.Name, false) - - if s.https != nil && common.HTTP3Enabled { - if s.proxyProto { - // TODO: support proxy protocol for HTTP/3 - s.l.Warn().Msg("HTTP/3 is enabled, but proxy protocol is yet not supported for HTTP/3") - } else { - s.https.TLSConfig.NextProtos = []string{http3.NextProtoH3, "h2", "http/1.1"} - h3 := &http3.Server{ - Addr: s.https.Addr, - Handler: s.https.Handler, - TLSConfig: http3.ConfigureTLSConfig(s.https.TLSConfig), - } - Start(subtask, h3, WithProxyProtocolSupport(s.proxyProto), WithACL(s.acl), WithLogger(&s.l)) - if s.http != nil { - s.http.Handler = advertiseHTTP3(s.http.Handler, h3) - } - // s.https is not nil (checked above) - s.https.Handler = advertiseHTTP3(s.https.Handler, h3) - } - } - - Start(subtask, s.http, WithProxyProtocolSupport(s.proxyProto), WithACL(s.acl), WithLogger(&s.l)) - Start(subtask, s.https, WithProxyProtocolSupport(s.proxyProto), WithACL(s.acl), WithLogger(&s.l)) -} - -type ServerStartOptions struct { - tcpWrappers []func(l net.Listener) net.Listener - udpWrappers []func(l net.PacketConn) net.PacketConn - logger *zerolog.Logger - proxyProto bool -} - -type ServerStartOption func(opts *ServerStartOptions) - -func WithTCPWrappers(wrappers ...func(l net.Listener) net.Listener) ServerStartOption { - return func(opts *ServerStartOptions) { - opts.tcpWrappers = wrappers - } -} - -func WithUDPWrappers(wrappers ...func(l net.PacketConn) net.PacketConn) ServerStartOption { - return func(opts *ServerStartOptions) { - opts.udpWrappers = wrappers - } -} - -func WithLogger(logger *zerolog.Logger) ServerStartOption { - return func(opts *ServerStartOptions) { - opts.logger = logger - } -} - -func WithACL(acl *acl.Config) ServerStartOption { - return func(opts *ServerStartOptions) { - if acl == nil { - return - } - opts.tcpWrappers = append(opts.tcpWrappers, acl.WrapTCP) - opts.udpWrappers = append(opts.udpWrappers, acl.WrapUDP) - } -} - -func WithProxyProtocolSupport(value bool) ServerStartOption { - return func(opts *ServerStartOptions) { - opts.proxyProto = value - } -} - -func Start[Server httpServer](parent task.Parent, srv Server, optFns ...ServerStartOption) (port int) { - if srv == nil { - return port - } - - var opts ServerStartOptions - for _, optFn := range optFns { - optFn(&opts) - } - if opts.logger == nil { - opts.logger = &log.Logger - } - - setDebugLogger(srv, opts.logger) - - proto := proto(srv) - task := parent.Subtask(proto, true) - - var lc net.ListenConfig - var serveFunc func() error - - switch srv := any(srv).(type) { - case *http.Server: - srv.BaseContext = func(l net.Listener) context.Context { - return parent.Context() - } - l, err := lc.Listen(task.Context(), "tcp", srv.Addr) - if err != nil { - HandleError(opts.logger, err, "failed to listen on port") - return port - } - port = l.Addr().(*net.TCPAddr).Port - if opts.proxyProto { - l = &proxyproto.Listener{Listener: l} - } - if srv.TLSConfig != nil { - l = tls.NewListener(l, srv.TLSConfig) - } - for _, wrapper := range opts.tcpWrappers { - l = wrapper(l) - } - if opts.proxyProto { - serveFunc = getServeFunc(l, h2proxy.NewServer(srv, nil).Serve) - } else { - serveFunc = getServeFunc(l, srv.Serve) - } - task.OnCancel("stop", func() { - stop(srv, l, opts.logger) - }) - case *http3.Server: - l, err := lc.ListenPacket(task.Context(), "udp", srv.Addr) - if err != nil { - HandleError(opts.logger, err, "failed to listen on port") - return port - } - port = l.LocalAddr().(*net.UDPAddr).Port - for _, wrapper := range opts.udpWrappers { - l = wrapper(l) - } - serveFunc = getServeFunc(l, srv.Serve) - task.OnCancel("stop", func() { - stop(srv, l, opts.logger) - }) - } - logStarted(srv, opts.logger) - go func() { - err := convertError(serveFunc()) - if err != nil { - HandleError(opts.logger, err, "failed to serve "+proto+" server") - } - task.Finish(err) - }() - return port -} - -func stop[Server httpServer](srv Server, l io.Closer, logger *zerolog.Logger) { - if srv == nil { - return - } - - proto := proto(srv) - - ctx, cancel := context.WithTimeout(task.RootContext(), 1*time.Second) - defer cancel() - - if err := convertError(errors.Join(srv.Shutdown(ctx), l.Close())); err != nil { - HandleError(logger, err, "failed to shutdown "+proto+" server") - } else { - logger.Info().Str("proto", proto).Str("addr", addr(srv)).Msg("server stopped") - } -} - -func (s *Server) Uptime() time.Duration { - return time.Since(s.startTime) -} diff --git a/internal/net/gphttp/server/utils.go b/internal/net/gphttp/server/utils.go deleted file mode 100644 index 57f9a7fd..00000000 --- a/internal/net/gphttp/server/utils.go +++ /dev/null @@ -1,89 +0,0 @@ -package server - -import ( - "context" - "errors" - "log" - "log/slog" - "net/http" - "syscall" - - "github.com/quic-go/quic-go/http3" - "github.com/rs/zerolog" - slogzerolog "github.com/samber/slog-zerolog/v2" - "github.com/yusing/godoxy/internal/common" - "github.com/yusing/godoxy/internal/net/gphttp" - "github.com/yusing/goutils/http/httpheaders" -) - -func advertiseHTTP3(handler http.Handler, h3 *http3.Server) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.ProtoMajor < 3 { - err := h3.SetQUICHeaders(w.Header()) - if err != nil { - switch { - case errors.Is(err, context.Canceled), - errors.Is(err, syscall.EPIPE), - errors.Is(err, syscall.ECONNRESET): - return - } - gphttp.LogError(r).Msg(err.Error()) - if httpheaders.IsWebsocket(r.Header) { - return - } - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - } - handler.ServeHTTP(w, r) - }) -} - -func proto[Server httpServer](srv Server) string { - var proto string - switch src := any(srv).(type) { - case *http.Server: - if src.TLSConfig == nil { - proto = "http" - } else { - proto = "https" - } - case *http3.Server: - proto = "h3" - } - return proto -} - -func addr[Server httpServer](srv Server) string { - var addr string - switch src := any(srv).(type) { - case *http.Server: - addr = src.Addr - case *http3.Server: - addr = src.Addr - } - return addr -} - -func getServeFunc[listener any](l listener, serve func(listener) error) func() error { - return func() error { - return serve(l) - } -} - -func setDebugLogger[Server httpServer](srv Server, logger *zerolog.Logger) { - if !common.IsDebug { - return - } - switch srv := any(srv).(type) { - case *http.Server: - srv.ErrorLog = log.New(logger, "", 0) - case *http3.Server: - logOpts := slogzerolog.Option{Level: slog.LevelDebug, Logger: logger} - srv.Logger = slog.New(logOpts.NewZerologHandler()) - } -} - -func logStarted[Server httpServer](srv Server, logger *zerolog.Logger) { - logger.Info().Str("proto", proto(srv)).Str("addr", addr(srv)).Msg("server started") -} diff --git a/internal/net/gphttp/status_code.go b/internal/net/gphttp/status_code.go deleted file mode 100644 index 25977df7..00000000 --- a/internal/net/gphttp/status_code.go +++ /dev/null @@ -1,11 +0,0 @@ -package gphttp - -import "net/http" - -func IsSuccess(status int) bool { - return status >= http.StatusOK && status < http.StatusMultipleChoices -} - -func IsStatusCodeValid(status int) bool { - return http.StatusText(status) != "" -} diff --git a/internal/net/gphttp/websocket/manager.go b/internal/net/gphttp/websocket/manager.go deleted file mode 100644 index 1ed61046..00000000 --- a/internal/net/gphttp/websocket/manager.go +++ /dev/null @@ -1,327 +0,0 @@ -package websocket - -import ( - "compress/flate" - "context" - "encoding/json" - "errors" - "fmt" - "net" - "net/http" - "net/url" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" - "github.com/rs/zerolog/log" - "github.com/yusing/godoxy/internal/common" -) - -// Manager handles WebSocket connection state and ping-pong -type Manager struct { - conn *websocket.Conn - ctx context.Context - cancel context.CancelFunc - pongWriteTimeout time.Duration - pingCheckTicker *time.Ticker - lastPingTime atomic.Value - readCh chan []byte - err error - - writeLock sync.Mutex - closeOnce sync.Once -} - -var defaultUpgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - origin := r.Header.Get("Origin") - if origin == "" { - return true - } - u, err := url.Parse(origin) - if err != nil { - return false - } - if u.Host == "" { - return false - } - originHost := strings.ToLower(u.Hostname()) - reqHost := r.Host - if h, _, e := net.SplitHostPort(reqHost); e == nil { - reqHost = h - } - if reqHost == "127.0.0.1" || reqHost == "localhost" { - return true - } - reqHost = strings.ToLower(reqHost) - return originHost == reqHost - }, - EnableCompression: true, -} - -var ( - ErrReadTimeout = errors.New("read timeout") - ErrWriteTimeout = errors.New("write timeout") -) - -const ( - TextMessage = websocket.TextMessage - BinaryMessage = websocket.BinaryMessage -) - -// NewManagerWithUpgrade upgrades the HTTP connection to a WebSocket connection and returns a Manager. -// If the upgrade fails, the error is returned. -// If the upgrade succeeds, the Manager is returned. -// -// To use a custom upgrader, set the "upgrader" context value to the upgrader. -func NewManagerWithUpgrade(c *gin.Context) (*Manager, error) { - actualUpgrader := &defaultUpgrader - if upgrader, ok := c.Get("upgrader"); ok { - actualUpgrader = upgrader.(*websocket.Upgrader) - } - - conn, err := actualUpgrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - return nil, err - } - - conn.EnableWriteCompression(true) - _ = conn.SetCompressionLevel(flate.BestSpeed) - - ctx, cancel := context.WithCancel(c.Request.Context()) - cm := &Manager{ - conn: conn, - ctx: ctx, - cancel: cancel, - pongWriteTimeout: 2 * time.Second, - pingCheckTicker: time.NewTicker(3 * time.Second), - readCh: make(chan []byte, 1), - } - cm.lastPingTime.Store(time.Now()) - - conn.SetCloseHandler(func(code int, text string) error { - if common.IsDebug { - cm.err = fmt.Errorf("connection closed: code=%d, text=%s", code, text) - } - cm.Close() - return nil - }) - - go cm.pingCheckRoutine() - go cm.readRoutine() - - // Ensure resources are released when parent context is canceled. - go func() { - <-ctx.Done() - cm.Close() - }() - - return cm, nil -} - -func (cm *Manager) Context() context.Context { - return cm.ctx -} - -// Periodic writes data to the connection periodically, with deduplication. -// If the connection is closed, the error is returned. -// If the write timeout is reached, ErrWriteTimeout is returned. -func (cm *Manager) PeriodicWrite(interval time.Duration, getData func() (any, error), deduplicate ...DeduplicateFunc) error { - var lastData any - - var equals DeduplicateFunc - if len(deduplicate) > 0 { - equals = deduplicate[0] - } - - write := func() { - data, err := getData() - if err != nil { - cm.err = err - cm.Close() - return - } - - // skip if the data is the same as the last data - if equals != nil && equals(data, lastData) { - return - } - - lastData = data - - if err := cm.WriteJSON(data, interval); err != nil { - cm.err = err - cm.Close() - } - } - - // initial write before the ticker starts - write() - if cm.err != nil { - return cm.err - } - - ticker := time.NewTicker(interval) - defer ticker.Stop() - for { - select { - case <-cm.ctx.Done(): - return cm.err - case <-ticker.C: - write() - if cm.err != nil { - return cm.err - } - } - } -} - -// WriteJSON writes a JSON message to the connection with json. -// If the connection is closed, the error is returned. -// If the write timeout is reached, ErrWriteTimeout is returned. -func (cm *Manager) WriteJSON(data any, timeout time.Duration) error { - bytes, err := json.Marshal(data) - if err != nil { - return err - } - return cm.WriteData(websocket.TextMessage, bytes, timeout) -} - -// WriteData writes a message to the connection with sonic. -// If the connection is closed, the error is returned. -// If the write timeout is reached, ErrWriteTimeout is returned. -func (cm *Manager) WriteData(typ int, data []byte, timeout time.Duration) error { - select { - case <-cm.ctx.Done(): - return cm.err - default: - cm.writeLock.Lock() - defer cm.writeLock.Unlock() - - if err := cm.conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil { - return err - } - err := cm.conn.WriteMessage(typ, data) - if err != nil { - if errors.Is(err, websocket.ErrCloseSent) { - return cm.err - } - if errors.Is(err, context.DeadlineExceeded) { - return ErrWriteTimeout - } - return err - } - return nil - } -} - -// ReadJSON reads a JSON message from the connection and unmarshals it into the provided struct with sonic -// If the connection is closed, the error is returned. -// If the message fails to unmarshal, the error is returned. -// If the read timeout is reached, ErrReadTimeout is returned. -func (cm *Manager) ReadJSON(out any, timeout time.Duration) error { - select { - case <-cm.ctx.Done(): - return cm.err - case data := <-cm.readCh: - return json.Unmarshal(data, out) - case <-time.After(timeout): - return ErrReadTimeout - } -} - -func (cm *Manager) ReadBinary(timeout time.Duration) ([]byte, error) { - select { - case <-cm.ctx.Done(): - return nil, cm.err - case data := <-cm.readCh: - return data, nil - case <-time.After(timeout): - return nil, ErrReadTimeout - } -} - -// Close closes the connection and cancels the context -func (cm *Manager) Close() { - cm.closeOnce.Do(cm.close) -} - -func (cm *Manager) close() { - cm.cancel() - - cm.writeLock.Lock() - defer cm.writeLock.Unlock() - - _ = cm.conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) - _ = cm.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) - cm.conn.Close() - - cm.pingCheckTicker.Stop() - - if cm.err != nil { - log.Debug().Caller(4).Msg("Closing WebSocket connection: " + cm.err.Error()) - } else { - log.Debug().Caller(4).Msg("Closing WebSocket connection") - } -} - -// Done returns a channel that is closed when the context is done or the connection is closed -func (cm *Manager) Done() <-chan struct{} { - return cm.ctx.Done() -} - -func (cm *Manager) pingCheckRoutine() { - for { - select { - case <-cm.ctx.Done(): - return - case <-cm.pingCheckTicker.C: - if time.Since(cm.lastPingTime.Load().(time.Time)) > 5*time.Second { - if common.IsDebug { - cm.err = errors.New("no ping received in 5 seconds, closing connection") - } - cm.Close() - return - } - } - } -} - -func (cm *Manager) readRoutine() { - for { - select { - case <-cm.ctx.Done(): - return - default: - typ, data, err := cm.conn.ReadMessage() - if err != nil { - if cm.ctx.Err() == nil { // connection is not closed - cm.err = fmt.Errorf("failed to read message: %w", err) - cm.Close() - } - return - } - - if typ == websocket.TextMessage && string(data) == "ping" { - cm.lastPingTime.Store(time.Now()) - if err := cm.WriteData(websocket.TextMessage, []byte("pong"), cm.pongWriteTimeout); err != nil { - cm.err = fmt.Errorf("failed to write pong message: %w", err) - cm.Close() - return - } - continue - } - - if typ == websocket.TextMessage || typ == websocket.BinaryMessage { - select { - case <-cm.ctx.Done(): - return - case cm.readCh <- data: - } - } - } - } -} diff --git a/internal/net/gphttp/websocket/reader.go b/internal/net/gphttp/websocket/reader.go deleted file mode 100644 index fd8b1133..00000000 --- a/internal/net/gphttp/websocket/reader.go +++ /dev/null @@ -1,25 +0,0 @@ -package websocket - -import ( - "io" - "time" -) - -type Reader struct { - manager *Manager -} - -func (m *Manager) NewReader() io.Reader { - return &Reader{ - manager: m, - } -} - -func (r *Reader) Read(p []byte) (int, error) { - data, err := r.manager.ReadBinary(10 * time.Second) - if err != nil { - return 0, err - } - copy(p, data) - return len(data), nil -} diff --git a/internal/net/gphttp/websocket/utils.go b/internal/net/gphttp/websocket/utils.go deleted file mode 100644 index 63fe32ea..00000000 --- a/internal/net/gphttp/websocket/utils.go +++ /dev/null @@ -1,23 +0,0 @@ -package websocket - -import ( - "time" - - "github.com/gin-gonic/gin" - apitypes "github.com/yusing/godoxy/internal/api/types" -) - -type DeduplicateFunc func(last, current any) bool - -func PeriodicWrite(c *gin.Context, interval time.Duration, get func() (any, error), deduplicate ...DeduplicateFunc) { - manager, err := NewManagerWithUpgrade(c) - if err != nil { - c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket")) - return - } - defer manager.Close() - err = manager.PeriodicWrite(interval, get, deduplicate...) - if err != nil { - c.Error(apitypes.InternalServerError(err, "failed to write to websocket")) - } -} diff --git a/internal/net/gphttp/websocket/writer.go b/internal/net/gphttp/websocket/writer.go deleted file mode 100644 index 0fd8a4ce..00000000 --- a/internal/net/gphttp/websocket/writer.go +++ /dev/null @@ -1,22 +0,0 @@ -package websocket - -import ( - "io" - "time" -) - -type Writer struct { - msgType int - manager *Manager -} - -func (cm *Manager) NewWriter(msgType int) io.Writer { - return &Writer{ - msgType: msgType, - manager: cm, - } -} - -func (w *Writer) Write(p []byte) (int, error) { - return len(p), w.manager.WriteData(w.msgType, p, 10*time.Second) -} diff --git a/internal/route/rules/do.go b/internal/route/rules/do.go index 1eae30c0..21e6c801 100644 --- a/internal/route/rules/do.go +++ b/internal/route/rules/do.go @@ -9,6 +9,7 @@ import ( gphttp "github.com/yusing/godoxy/internal/net/gphttp" nettypes "github.com/yusing/godoxy/internal/net/types" gperr "github.com/yusing/goutils/errs" + httputils "github.com/yusing/goutils/http" "github.com/yusing/goutils/http/reverseproxy" strutils "github.com/yusing/goutils/strings" ) @@ -118,7 +119,7 @@ var commands = map[string]struct { if err != nil { return nil, ErrInvalidArguments.With(err) } - if !gphttp.IsStatusCodeValid(code) { + if !httputils.IsStatusCodeValid(code) { return nil, ErrInvalidArguments.Subject(codeStr) } return &Tuple[int, string]{code, text}, nil diff --git a/internal/route/rules/validate.go b/internal/route/rules/validate.go index 0736089f..7d52eeaf 100644 --- a/internal/route/rules/validate.go +++ b/internal/route/rules/validate.go @@ -8,9 +8,9 @@ import ( "strings" "github.com/gobwas/glob" - gphttp "github.com/yusing/godoxy/internal/net/gphttp" nettypes "github.com/yusing/godoxy/internal/net/types" gperr "github.com/yusing/goutils/errs" + httputils "github.com/yusing/goutils/http" ) type ( @@ -169,7 +169,7 @@ func validateMethod(args []string) (any, gperr.Error) { return nil, ErrExpectOneArg } method := strings.ToUpper(args[0]) - if !gphttp.IsMethodValid(method) { + if !httputils.IsMethodValid(method) { return nil, ErrInvalidArguments.Subject(method) } return method, nil