mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-01 06:33:18 +02:00
refactor: move websocket package and some http utils to seperate repo
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 "/"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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) }
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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) != ""
|
||||
}
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user