refactor: move websocket package and some http utils to seperate repo

This commit is contained in:
yusing
2025-09-27 14:16:42 +08:00
parent 6776f20332
commit 2a05c6a630
48 changed files with 52 additions and 1193 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"
)

View File

@@ -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 (

View File

@@ -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

View File

@@ -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 {

View File

@@ -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"
)

View File

@@ -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"

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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"
)

View File

@@ -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

View File

@@ -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=

View File

@@ -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 "/"

View File

@@ -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
}

View File

@@ -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 (

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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())
}

View File

@@ -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) }

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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))
})
}

View File

@@ -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)

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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()
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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")
}

View File

@@ -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) != ""
}

View File

@@ -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:
}
}
}
}
}

View File

@@ -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
}

View File

@@ -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"))
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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