mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 09:48:49 +02:00
refactor: move websocket package and some http utils to seperate repo
This commit is contained in:
@@ -9,9 +9,9 @@ import (
|
|||||||
"github.com/yusing/godoxy/agent/pkg/env"
|
"github.com/yusing/godoxy/agent/pkg/env"
|
||||||
"github.com/yusing/godoxy/agent/pkg/server"
|
"github.com/yusing/godoxy/agent/pkg/server"
|
||||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||||
httpServer "github.com/yusing/godoxy/internal/net/gphttp/server"
|
|
||||||
"github.com/yusing/godoxy/pkg"
|
"github.com/yusing/godoxy/pkg"
|
||||||
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
||||||
|
httpServer "github.com/yusing/goutils/server"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ require (
|
|||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/yusing/godoxy v0.18.6
|
github.com/yusing/godoxy v0.18.6
|
||||||
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||||
github.com/yusing/goutils v0.3.1
|
github.com/yusing/goutils v0.4.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -69,7 +69,6 @@ require (
|
|||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
@@ -82,7 +81,6 @@ require (
|
|||||||
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
|
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
|
||||||
github.com/shirou/gopsutil/v4 v4.25.8 // indirect
|
github.com/shirou/gopsutil/v4 v4.25.8 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
@@ -107,7 +105,6 @@ require (
|
|||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/text v0.29.0 // indirect
|
golang.org/x/text v0.29.0 // indirect
|
||||||
golang.org/x/time v0.13.0 // indirect
|
|
||||||
golang.org/x/tools v0.37.0 // indirect
|
golang.org/x/tools v0.37.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -208,8 +208,8 @@ github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yusing/ds v0.2.0 h1:lPhDU5eA2uvquVrBrzLCrQXRJJgSXlUYA53TbuK2sQY=
|
github.com/yusing/ds v0.2.0 h1:lPhDU5eA2uvquVrBrzLCrQXRJJgSXlUYA53TbuK2sQY=
|
||||||
github.com/yusing/ds v0.2.0/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
github.com/yusing/ds v0.2.0/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||||
github.com/yusing/goutils v0.3.1 h1:xCPoZ/haI8ZJ0ZaPU4g6+okSPdBczs8o98tIZ/TbpsQ=
|
github.com/yusing/goutils v0.4.1 h1:80uFNxXfm4zXMYDku0rWMLyqEiXO0UOMFOaUC4b/6fI=
|
||||||
github.com/yusing/goutils v0.3.1/go.mod h1:meg9GcAU8yvBY21JgYjPuLsXD1Q5VdVHE32A4tG5Y5g=
|
github.com/yusing/goutils v0.4.1/go.mod h1:xsoLWLtIiu7k+9Bn6azERDs5o3Djb3b2/DW1htHrOjg=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/agent/pkg/env"
|
"github.com/yusing/godoxy/agent/pkg/env"
|
||||||
"github.com/yusing/godoxy/agent/pkg/handler"
|
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/server"
|
"github.com/yusing/goutils/server"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
6
go.mod
6
go.mod
@@ -42,15 +42,15 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/luthermonson/go-proxmox v0.2.3
|
github.com/luthermonson/go-proxmox v0.2.3
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1
|
github.com/oschwald/maxminddb-golang v1.13.1
|
||||||
github.com/quic-go/quic-go v0.54.1 // http3 support
|
github.com/quic-go/quic-go v0.54.1 // indirect; http3 support
|
||||||
github.com/samber/slog-zerolog/v2 v2.7.3
|
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
|
||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/yusing/ds v0.2.0
|
github.com/yusing/ds v0.2.0
|
||||||
github.com/yusing/godoxy/agent v0.0.0-20250926130035-55c1c918ba95
|
github.com/yusing/godoxy/agent v0.0.0-20250926130035-55c1c918ba95
|
||||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20250926130035-55c1c918ba95
|
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20250926130035-55c1c918ba95
|
||||||
github.com/yusing/godoxy/internal/utils v0.1.0
|
github.com/yusing/godoxy/internal/utils v0.1.0
|
||||||
github.com/yusing/goutils v0.3.1
|
github.com/yusing/goutils v0.4.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1648,8 +1648,8 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yusing/ds v0.2.0 h1:lPhDU5eA2uvquVrBrzLCrQXRJJgSXlUYA53TbuK2sQY=
|
github.com/yusing/ds v0.2.0 h1:lPhDU5eA2uvquVrBrzLCrQXRJJgSXlUYA53TbuK2sQY=
|
||||||
github.com/yusing/ds v0.2.0/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
github.com/yusing/ds v0.2.0/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||||
github.com/yusing/goutils v0.3.1 h1:xCPoZ/haI8ZJ0ZaPU4g6+okSPdBczs8o98tIZ/TbpsQ=
|
github.com/yusing/goutils v0.4.1 h1:80uFNxXfm4zXMYDku0rWMLyqEiXO0UOMFOaUC4b/6fI=
|
||||||
github.com/yusing/goutils v0.3.1/go.mod h1:meg9GcAU8yvBY21JgYjPuLsXD1Q5VdVHE32A4tG5Y5g=
|
github.com/yusing/goutils v0.4.1/go.mod h1:xsoLWLtIiu7k+9Bn6azERDs5o3Djb3b2/DW1htHrOjg=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"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/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "list"
|
// @x-id "list"
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
apitypes "github.com/yusing/godoxy/internal/api/types"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/websocket"
|
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "renew"
|
// @x-id "renew"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
apitypes "github.com/yusing/godoxy/internal/api/types"
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"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"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
apitypes "github.com/yusing/godoxy/internal/api/types"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/websocket"
|
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/websocket"
|
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HealthMap = map[string]routes.HealthInfo // @name HealthMap
|
type HealthMap = map[string]routes.HealthInfo // @name HealthMap
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
apitypes "github.com/yusing/godoxy/internal/api/types"
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/websocket"
|
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HomepageItemsRequest struct {
|
type HomepageItemsRequest struct {
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import (
|
|||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
apitypes "github.com/yusing/godoxy/internal/api/types"
|
||||||
"github.com/yusing/godoxy/internal/metrics/period"
|
"github.com/yusing/godoxy/internal/metrics/period"
|
||||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/websocket"
|
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
"github.com/yusing/goutils/synk"
|
"github.com/yusing/goutils/synk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
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/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "providers"
|
// @x-id "providers"
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"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"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RouteType route.Route // @name Route
|
type RouteType route.Route // @name Route
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
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/godoxy/internal/types"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StatsResponse struct {
|
type StatsResponse struct {
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import (
|
|||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp"
|
|
||||||
"github.com/yusing/godoxy/internal/utils"
|
"github.com/yusing/godoxy/internal/utils"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/time/rate"
|
"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))
|
oauth2Token, err := auth.oauthConfig.Exchange(r.Context(), code, optRedirectPostAuth(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
idTokenJWT, idToken, err := auth.getIDToken(r.Context(), oauth2Token)
|
idTokenJWT, idToken, err := auth.getIDToken(r.Context(), oauth2Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +333,7 @@ func (auth *OIDCProvider) PostAuthCallbackHandler(w http.ResponseWriter, r *http
|
|||||||
claims, err := parseClaims(idToken)
|
claims, err := parseClaims(idToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
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
|
return
|
||||||
}
|
}
|
||||||
session := newSession(claims.Username, claims.Groups)
|
session := newSession(claims.Username, claims.Groups)
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp"
|
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -122,7 +122,7 @@ func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http
|
|||||||
token, err := auth.NewToken()
|
token, err := auth.NewToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
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
|
return
|
||||||
}
|
}
|
||||||
SetTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL)
|
SetTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL)
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ var (
|
|||||||
IsDebug = env.GetEnvBool("DEBUG", IsTest)
|
IsDebug = env.GetEnvBool("DEBUG", IsTest)
|
||||||
IsTrace = env.GetEnvBool("TRACE", false) && IsDebug
|
IsTrace = env.GetEnvBool("TRACE", false) && IsDebug
|
||||||
|
|
||||||
HTTP3Enabled = env.GetEnvBool("HTTP3_ENABLED", true)
|
|
||||||
|
|
||||||
ProxyHTTPAddr,
|
ProxyHTTPAddr,
|
||||||
ProxyHTTPHost,
|
ProxyHTTPHost,
|
||||||
ProxyHTTPPort,
|
ProxyHTTPPort,
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
config "github.com/yusing/godoxy/internal/config/types"
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
"github.com/yusing/godoxy/internal/entrypoint"
|
"github.com/yusing/godoxy/internal/entrypoint"
|
||||||
"github.com/yusing/godoxy/internal/maxmind"
|
"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/notif"
|
||||||
"github.com/yusing/godoxy/internal/proxmox"
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
proxy "github.com/yusing/godoxy/internal/route/provider"
|
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"
|
||||||
"github.com/yusing/godoxy/internal/watcher/events"
|
"github.com/yusing/godoxy/internal/watcher/events"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
"github.com/yusing/goutils/server"
|
||||||
"github.com/yusing/goutils/strings/ansi"
|
"github.com/yusing/goutils/strings/ansi"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ require (
|
|||||||
github.com/vultr/govultr/v3 v3.24.0 // indirect
|
github.com/vultr/govultr/v3 v3.24.0 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
github.com/yusing/godoxy/internal/utils v0.1.0 // 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/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
go.opentelemetry.io/otel 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.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.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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.4.1 h1:80uFNxXfm4zXMYDku0rWMLyqEiXO0UOMFOaUC4b/6fI=
|
||||||
github.com/yusing/goutils v0.3.1/go.mod h1:meg9GcAU8yvBY21JgYjPuLsXD1Q5VdVHE32A4tG5Y5g=
|
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/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
"github.com/vincent-petithory/dataurl"
|
"github.com/vincent-petithory/dataurl"
|
||||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
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 &FetchResult{StatusCode: c.status, ErrMsg: "upstream error: " + string(c.data)}
|
||||||
}
|
}
|
||||||
// return icon 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")}
|
return &FetchResult{Icon: c.data, contentType: c.header.Get("Content-Type")}
|
||||||
}
|
}
|
||||||
// try extract from "link[rel=icon]" from path "/"
|
// try extract from "link[rel=icon]" from path "/"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
api "github.com/yusing/godoxy/internal/api/v1"
|
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"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
accept := gphttp.GetAccept(r.Header)
|
accept := httputils.GetAccept(r.Header)
|
||||||
acceptHTML := (r.Method == http.MethodGet && accept.AcceptHTML() || r.RequestURI == "/" && accept.IsEmpty())
|
acceptHTML := (r.Method == http.MethodGet && accept.AcceptHTML() || r.RequestURI == "/" && accept.IsEmpty())
|
||||||
|
|
||||||
isCheckRedirect := r.Header.Get(httpheaders.HeaderGoDoxyCheckRedirect) != ""
|
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)
|
err := w.Wake(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
|
|
||||||
. "github.com/yusing/godoxy/internal/logging/accesslog"
|
. "github.com/yusing/godoxy/internal/logging/accesslog"
|
||||||
"github.com/yusing/godoxy/internal/utils"
|
"github.com/yusing/godoxy/internal/utils"
|
||||||
expect "github.com/yusing/goutils/testing"
|
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
|
expect "github.com/yusing/goutils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/puzpuzpuz/xsync/v4"
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
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 {
|
type logEntryRange struct {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
apitypes "github.com/yusing/godoxy/internal/api/types"
|
||||||
metricsutils "github.com/yusing/godoxy/internal/metrics/utils"
|
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/httpheaders"
|
||||||
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResponseType[AggregateT any] struct {
|
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/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/auth"
|
"github.com/yusing/godoxy/internal/auth"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp"
|
httputils "github.com/yusing/goutils/http"
|
||||||
|
|
||||||
_ "embed"
|
_ "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)
|
http.Error(w, "Captcha is required", http.StatusForbidden)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/puzpuzpuz/xsync/v4"
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
|
||||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -35,7 +35,7 @@ var (
|
|||||||
func init() {
|
func init() {
|
||||||
serialization.MustRegisterValidation("status_code", func(fl validator.FieldLevel) bool {
|
serialization.MustRegisterValidation("status_code", func(fl validator.FieldLevel) bool {
|
||||||
statusCode := fl.Field().Int()
|
statusCode := fl.Field().Int()
|
||||||
return gphttp.IsStatusCodeValid(int(statusCode))
|
return httputils.IsStatusCodeValid(int(statusCode))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware/errorpage"
|
"github.com/yusing/godoxy/internal/net/gphttp/middleware/errorpage"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,8 +28,8 @@ func (customErrorPage) before(w http.ResponseWriter, r *http.Request) (proceed b
|
|||||||
// modifyResponse implements ResponseModifier.
|
// modifyResponse implements ResponseModifier.
|
||||||
func (customErrorPage) modifyResponse(resp *http.Response) error {
|
func (customErrorPage) modifyResponse(resp *http.Response) error {
|
||||||
// only handles non-success status code and html/plain content type
|
// only handles non-success status code and html/plain content type
|
||||||
contentType := gphttp.GetContentType(resp.Header)
|
contentType := httputils.GetContentType(resp.Header)
|
||||||
if !gphttp.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) {
|
if !httputils.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) {
|
||||||
errorPage, ok := errorpage.GetErrorPageByStatus(resp.StatusCode)
|
errorPage, ok := errorpage.GetErrorPageByStatus(resp.StatusCode)
|
||||||
if ok {
|
if ok {
|
||||||
log.Debug().Msgf("error page for status %d loaded", resp.StatusCode)
|
log.Debug().Msgf("error page for status %d loaded", resp.StatusCode)
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
"github.com/yusing/goutils/http/reverseproxy"
|
"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) {
|
func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r *http.Request) {
|
||||||
if exec, ok := m.impl.(ResponseModifier); ok {
|
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)
|
return exec.modifyResponse(resp)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
|
||||||
httputils "github.com/yusing/goutils/http"
|
httputils "github.com/yusing/goutils/http"
|
||||||
ioutils "github.com/yusing/goutils/io"
|
ioutils "github.com/yusing/goutils/io"
|
||||||
"github.com/yusing/goutils/synk"
|
"github.com/yusing/goutils/synk"
|
||||||
@@ -36,7 +35,7 @@ func (m *modifyHTML) before(_ http.ResponseWriter, req *http.Request) bool {
|
|||||||
// modifyResponse implements ResponseModifier.
|
// modifyResponse implements ResponseModifier.
|
||||||
func (m *modifyHTML) modifyResponse(resp *http.Response) error {
|
func (m *modifyHTML) modifyResponse(resp *http.Response) error {
|
||||||
// including text/html and application/xhtml+xml
|
// including text/html and application/xhtml+xml
|
||||||
if !gphttp.GetContentType(resp.Header).IsHTML() {
|
if !httputils.GetContentType(resp.Header).IsHTML() {
|
||||||
return nil
|
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"
|
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
"github.com/yusing/goutils/http/reverseproxy"
|
"github.com/yusing/goutils/http/reverseproxy"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
)
|
)
|
||||||
@@ -118,7 +119,7 @@ var commands = map[string]struct {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidArguments.With(err)
|
return nil, ErrInvalidArguments.With(err)
|
||||||
}
|
}
|
||||||
if !gphttp.IsStatusCodeValid(code) {
|
if !httputils.IsStatusCodeValid(code) {
|
||||||
return nil, ErrInvalidArguments.Subject(codeStr)
|
return nil, ErrInvalidArguments.Subject(codeStr)
|
||||||
}
|
}
|
||||||
return &Tuple[int, string]{code, text}, nil
|
return &Tuple[int, string]{code, text}, nil
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gobwas/glob"
|
"github.com/gobwas/glob"
|
||||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
|
||||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
httputils "github.com/yusing/goutils/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -169,7 +169,7 @@ func validateMethod(args []string) (any, gperr.Error) {
|
|||||||
return nil, ErrExpectOneArg
|
return nil, ErrExpectOneArg
|
||||||
}
|
}
|
||||||
method := strings.ToUpper(args[0])
|
method := strings.ToUpper(args[0])
|
||||||
if !gphttp.IsMethodValid(method) {
|
if !httputils.IsMethodValid(method) {
|
||||||
return nil, ErrInvalidArguments.Subject(method)
|
return nil, ErrInvalidArguments.Subject(method)
|
||||||
}
|
}
|
||||||
return method, nil
|
return method, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user