mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 01:08:31 +02:00
refactor(entrypoint): move route registry into entrypoint context (#200)
- Introduced `NewTestRoute` function to simplify route creation in benchmark tests. - Replaced direct route validation and starting with error handling using `require.NoError`. - Updated server retrieval to use `common.ProxyHTTPAddr` for consistency. - Improved logging for HTTP route addition errors in `AddRoute` method. * fix(tcp): wrap proxy proto listener before acl * refactor(entrypoint): propagate errors from route registration and stream serving * fix(docs): correct swagger and package README
This commit is contained in:
@@ -158,10 +158,15 @@ Tips:
|
|||||||
},
|
},
|
||||||
ErrorLog: stdlog.New(&errLog, "", 0),
|
ErrorLog: stdlog.New(&errLog, "", 0),
|
||||||
}
|
}
|
||||||
srv.Serve(l)
|
go func() {
|
||||||
|
err := srv.Serve(l)
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Error().Err(err).Msg("socket proxy server stopped with error")
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
systeminfo.Poller.Start()
|
systeminfo.Poller.Start(t)
|
||||||
|
|
||||||
task.WaitExit(3)
|
task.WaitExit(3)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ require (
|
|||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||||
github.com/yusing/ds v0.4.1 // indirect
|
github.com/yusing/ds v0.4.1 // indirect
|
||||||
github.com/yusing/gointernals v0.1.16 // indirect
|
github.com/yusing/gointernals v0.1.18 // indirect
|
||||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468 // indirect
|
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468 // indirect
|
||||||
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468 // indirect
|
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
|||||||
@@ -210,8 +210,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
|||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
||||||
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
github.com/yusing/gointernals v0.1.18 h1:ou8/0tPURUgAOBJu3TN/iWF4S/5ZYQaap+rVkaJNUMw=
|
||||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
github.com/yusing/gointernals v0.1.18/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 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=
|
||||||
|
|||||||
25
cmd/main.go
25
cmd/main.go
@@ -1,12 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/api"
|
|
||||||
"github.com/yusing/godoxy/internal/auth"
|
"github.com/yusing/godoxy/internal/auth"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
"github.com/yusing/godoxy/internal/config"
|
"github.com/yusing/godoxy/internal/config"
|
||||||
@@ -14,12 +14,9 @@ import (
|
|||||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||||
"github.com/yusing/godoxy/internal/logging"
|
"github.com/yusing/godoxy/internal/logging"
|
||||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
|
||||||
"github.com/yusing/godoxy/internal/metrics/uptime"
|
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||||
"github.com/yusing/godoxy/internal/route/rules"
|
"github.com/yusing/godoxy/internal/route/rules"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
"github.com/yusing/goutils/server"
|
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
"github.com/yusing/goutils/version"
|
"github.com/yusing/goutils/version"
|
||||||
)
|
)
|
||||||
@@ -51,7 +48,6 @@ func main() {
|
|||||||
parallel(
|
parallel(
|
||||||
dnsproviders.InitProviders,
|
dnsproviders.InitProviders,
|
||||||
iconlist.InitCache,
|
iconlist.InitCache,
|
||||||
systeminfo.Poller.Start,
|
|
||||||
middleware.LoadComposeFiles,
|
middleware.LoadComposeFiles,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,32 +69,13 @@ func main() {
|
|||||||
gperr.LogWarn("errors in config", err)
|
gperr.LogWarn("errors in config", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.StartProxyServers()
|
|
||||||
|
|
||||||
if err := auth.Initialize(); err != nil {
|
if err := auth.Initialize(); err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||||
}
|
}
|
||||||
rules.InitAuthHandler(auth.AuthOrProceed)
|
rules.InitAuthHandler(auth.AuthOrProceed)
|
||||||
|
|
||||||
// API Handler needs to start after auth is initialized.
|
|
||||||
server.StartServer(task.RootTask("api_server", false), server.Options{
|
|
||||||
Name: "api",
|
|
||||||
HTTPAddr: common.APIHTTPAddr,
|
|
||||||
Handler: api.NewHandler(true),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Local API Handler is used for unauthenticated access.
|
|
||||||
if common.LocalAPIHTTPAddr != "" {
|
|
||||||
server.StartServer(task.RootTask("local_api_server", false), server.Options{
|
|
||||||
Name: "local_api",
|
|
||||||
HTTPAddr: common.LocalAPIHTTPAddr,
|
|
||||||
Handler: api.NewHandler(false),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
listenDebugServer()
|
listenDebugServer()
|
||||||
|
|
||||||
uptime.Poller.Start()
|
|
||||||
config.WatchChanges()
|
config.WatchChanges()
|
||||||
|
|
||||||
close(done)
|
close(done)
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -59,7 +59,7 @@ require (
|
|||||||
github.com/yusing/ds v0.4.1 // data structures and algorithms
|
github.com/yusing/ds v0.4.1 // data structures and algorithms
|
||||||
github.com/yusing/godoxy/agent v0.0.0-20260129101716-0f13004ad6ba
|
github.com/yusing/godoxy/agent v0.0.0-20260129101716-0f13004ad6ba
|
||||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260129101716-0f13004ad6ba
|
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260129101716-0f13004ad6ba
|
||||||
github.com/yusing/gointernals v0.1.16
|
github.com/yusing/gointernals v0.1.18
|
||||||
github.com/yusing/goutils v0.7.0
|
github.com/yusing/goutils v0.7.0
|
||||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468
|
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260129081554-24e52ede7468
|
||||||
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468
|
github.com/yusing/goutils/http/websocket v0.0.0-20260129081554-24e52ede7468
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -329,8 +329,8 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
github.com/yusing/ds v0.4.1 h1:syMCh7hO6Yw8xfcFkEaln3W+lVeWB/U/meYv6Wf2/Ig=
|
||||||
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
github.com/yusing/ds v0.4.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
github.com/yusing/gointernals v0.1.18 h1:ou8/0tPURUgAOBJu3TN/iWF4S/5ZYQaap+rVkaJNUMw=
|
||||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
github.com/yusing/gointernals v0.1.18/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 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=
|
||||||
|
|||||||
2
goutils
2
goutils
Submodule goutils updated: 52ea531e95...56663372de
@@ -74,8 +74,6 @@ type ipLog struct {
|
|||||||
allowed bool
|
allowed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContextKey struct{}
|
|
||||||
|
|
||||||
const cacheTTL = 1 * time.Minute
|
const cacheTTL = 1 * time.Minute
|
||||||
|
|
||||||
func (c *checkCache) Expired() bool {
|
func (c *checkCache) Expired() bool {
|
||||||
|
|||||||
9
internal/acl/types/acl.go
Normal file
9
internal/acl/types/acl.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
type ACL interface {
|
||||||
|
IPAllowed(ip net.IP) bool
|
||||||
|
WrapTCP(l net.Listener) net.Listener
|
||||||
|
WrapUDP(l net.PacketConn) net.PacketConn
|
||||||
|
}
|
||||||
16
internal/acl/types/context.go
Normal file
16
internal/acl/types/context.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package acl
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type ContextKey struct{}
|
||||||
|
|
||||||
|
func SetCtx(ctx interface{ SetValue(any, any) }, acl ACL) {
|
||||||
|
ctx.SetValue(ContextKey{}, acl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromCtx(ctx context.Context) ACL {
|
||||||
|
if acl, ok := ctx.Value(ContextKey{}).(ACL); ok {
|
||||||
|
return acl
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/moby/moby/api/types/container"
|
"github.com/moby/moby/api/types/container"
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
"github.com/yusing/godoxy/internal/docker"
|
"github.com/yusing/godoxy/internal/docker"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
@@ -44,7 +44,7 @@ func Stats(c *gin.Context) {
|
|||||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
||||||
if !ok {
|
if !ok {
|
||||||
var route types.Route
|
var route types.Route
|
||||||
route, ok = routes.GetIncludeExcluded(id)
|
route, ok = entrypoint.FromCtx(c.Request.Context()).GetRoute(id)
|
||||||
if ok {
|
if ok {
|
||||||
cont := route.ContainerInfo()
|
cont := route.ContainerInfo()
|
||||||
if cont == nil {
|
if cont == nil {
|
||||||
|
|||||||
@@ -1171,7 +1171,10 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "Health info by route name",
|
"description": "Health info by route name",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/HealthMap"
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/definitions/HealthStatusString"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"403": {
|
"403": {
|
||||||
@@ -1219,6 +1222,12 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/ErrorResponse"
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-id": "categories",
|
"x-id": "categories",
|
||||||
@@ -1337,6 +1346,12 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/ErrorResponse"
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ErrorResponse"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-id": "items",
|
"x-id": "items",
|
||||||
@@ -2820,43 +2835,6 @@
|
|||||||
"operationId": "tail"
|
"operationId": "tail"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/reload": {
|
|
||||||
"post": {
|
|
||||||
"description": "Reload config",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"v1"
|
|
||||||
],
|
|
||||||
"summary": "Reload config",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/SuccessResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-id": "reload",
|
|
||||||
"operationId": "reload"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/route/by_provider": {
|
"/route/by_provider": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List routes by provider",
|
"description": "List routes by provider",
|
||||||
@@ -4071,14 +4049,6 @@
|
|||||||
"x-nullable": false,
|
"x-nullable": false,
|
||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"HealthMap": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"$ref": "#/definitions/HealthStatusString"
|
|
||||||
},
|
|
||||||
"x-nullable": false,
|
|
||||||
"x-omitempty": false
|
|
||||||
},
|
|
||||||
"HealthStatusString": {
|
"HealthStatusString": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -5329,7 +5299,6 @@
|
|||||||
"x-omitempty": false
|
"x-omitempty": false
|
||||||
},
|
},
|
||||||
"bind": {
|
"bind": {
|
||||||
"description": "for TCP and UDP routes, bind address to listen on",
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-nullable": true
|
"x-nullable": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -419,10 +419,6 @@ definitions:
|
|||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
HealthMap:
|
|
||||||
additionalProperties:
|
|
||||||
$ref: '#/definitions/HealthStatusString'
|
|
||||||
type: object
|
|
||||||
HealthStatusString:
|
HealthStatusString:
|
||||||
enum:
|
enum:
|
||||||
- unknown
|
- unknown
|
||||||
@@ -1007,7 +1003,6 @@ definitions:
|
|||||||
alias:
|
alias:
|
||||||
type: string
|
type: string
|
||||||
bind:
|
bind:
|
||||||
description: for TCP and UDP routes, bind address to listen on
|
|
||||||
type: string
|
type: string
|
||||||
x-nullable: true
|
x-nullable: true
|
||||||
container:
|
container:
|
||||||
@@ -1807,12 +1802,12 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
kernel_version:
|
kernel_version:
|
||||||
type: string
|
type: string
|
||||||
|
load_avg_5m:
|
||||||
|
type: string
|
||||||
load_avg_15m:
|
load_avg_15m:
|
||||||
type: string
|
type: string
|
||||||
load_avg_1m:
|
load_avg_1m:
|
||||||
type: string
|
type: string
|
||||||
load_avg_5m:
|
|
||||||
type: string
|
|
||||||
mem_pct:
|
mem_pct:
|
||||||
type: string
|
type: string
|
||||||
mem_total:
|
mem_total:
|
||||||
@@ -2675,7 +2670,9 @@ paths:
|
|||||||
"200":
|
"200":
|
||||||
description: Health info by route name
|
description: Health info by route name
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/HealthMap'
|
additionalProperties:
|
||||||
|
$ref: '#/definitions/HealthStatusString'
|
||||||
|
type: object
|
||||||
"403":
|
"403":
|
||||||
description: Forbidden
|
description: Forbidden
|
||||||
schema:
|
schema:
|
||||||
@@ -2707,6 +2704,10 @@ paths:
|
|||||||
description: Forbidden
|
description: Forbidden
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ErrorResponse'
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
summary: List homepage categories
|
summary: List homepage categories
|
||||||
tags:
|
tags:
|
||||||
- homepage
|
- homepage
|
||||||
@@ -2784,6 +2785,10 @@ paths:
|
|||||||
description: Forbidden
|
description: Forbidden
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ErrorResponse'
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
summary: Homepage items
|
summary: Homepage items
|
||||||
tags:
|
tags:
|
||||||
- homepage
|
- homepage
|
||||||
@@ -3790,30 +3795,6 @@ paths:
|
|||||||
- proxmox
|
- proxmox
|
||||||
- websocket
|
- websocket
|
||||||
x-id: tail
|
x-id: tail
|
||||||
/reload:
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Reload config
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/SuccessResponse'
|
|
||||||
"403":
|
|
||||||
description: Forbidden
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ErrorResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ErrorResponse'
|
|
||||||
summary: Reload config
|
|
||||||
tags:
|
|
||||||
- v1
|
|
||||||
x-id: reload
|
|
||||||
/route/{which}:
|
/route/{which}:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||||
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
|
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
|
||||||
_ "unsafe"
|
_ "unsafe"
|
||||||
@@ -73,7 +73,11 @@ func FavIcon(c *gin.Context) {
|
|||||||
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
|
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
|
||||||
func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) {
|
func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) {
|
||||||
// try with route.Icon
|
// try with route.Icon
|
||||||
r, ok := routes.HTTP.Get(alias)
|
ep := entrypoint.FromCtx(ctx)
|
||||||
|
if ep == nil { // impossible, but just in case
|
||||||
|
return iconfetch.FetchResultWithErrorf(http.StatusInternalServerError, "entrypoint not initialized")
|
||||||
|
}
|
||||||
|
r, ok := ep.HTTPRoutes().Get(alias)
|
||||||
if !ok {
|
if !ok {
|
||||||
return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
|
"github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
|
||||||
_ "github.com/yusing/goutils/apitypes"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "health"
|
// @x-id "health"
|
||||||
@@ -19,16 +18,21 @@ import (
|
|||||||
// @Tags v1,websocket
|
// @Tags v1,websocket
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} routes.HealthMap "Health info by route name"
|
// @Success 200 {object} map[string]types.HealthStatusString "Health info by route name"
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /health [get]
|
// @Router /health [get]
|
||||||
func Health(c *gin.Context) {
|
func Health(c *gin.Context) {
|
||||||
|
ep := entrypoint.FromCtx(c.Request.Context())
|
||||||
|
if ep == nil { // impossible, but just in case
|
||||||
|
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||||
websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) {
|
websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) {
|
||||||
return routes.GetHealthInfoSimple(), nil
|
return ep.GetHealthInfoSimple(), nil
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, routes.GetHealthInfoSimple())
|
c.JSON(http.StatusOK, ep.GetHealthInfoSimple())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
|
|
||||||
_ "github.com/yusing/goutils/apitypes"
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "categories"
|
// @x-id "categories"
|
||||||
@@ -19,17 +20,23 @@ import (
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} string
|
// @Success 200 {array} string
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /homepage/categories [get]
|
// @Router /homepage/categories [get]
|
||||||
func Categories(c *gin.Context) {
|
func Categories(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, HomepageCategories())
|
ep := entrypoint.FromCtx(c.Request.Context())
|
||||||
|
if ep == nil { // impossible, but just in case
|
||||||
|
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, HomepageCategories(ep))
|
||||||
}
|
}
|
||||||
|
|
||||||
func HomepageCategories() []string {
|
func HomepageCategories(ep entrypoint.Entrypoint) []string {
|
||||||
check := make(map[string]struct{})
|
check := make(map[string]struct{})
|
||||||
categories := make([]string, 0)
|
categories := make([]string, 0)
|
||||||
categories = append(categories, homepage.CategoryAll)
|
categories = append(categories, homepage.CategoryAll)
|
||||||
categories = append(categories, homepage.CategoryFavorites)
|
categories = append(categories, homepage.CategoryFavorites)
|
||||||
for _, r := range routes.HTTP.Iter {
|
for _, r := range ep.HTTPRoutes().Iter {
|
||||||
item := r.HomepageItem()
|
item := r.HomepageItem()
|
||||||
if item.Category == "" {
|
if item.Category == "" {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
@@ -36,6 +36,7 @@ type HomepageItemsRequest struct {
|
|||||||
// @Success 200 {object} homepage.Homepage
|
// @Success 200 {object} homepage.Homepage
|
||||||
// @Failure 400 {object} apitypes.ErrorResponse
|
// @Failure 400 {object} apitypes.ErrorResponse
|
||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {object} apitypes.ErrorResponse
|
||||||
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /homepage/items [get]
|
// @Router /homepage/items [get]
|
||||||
func Items(c *gin.Context) {
|
func Items(c *gin.Context) {
|
||||||
var request HomepageItemsRequest
|
var request HomepageItemsRequest
|
||||||
@@ -53,29 +54,35 @@ func Items(c *gin.Context) {
|
|||||||
hostname = host
|
hostname = host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ep := entrypoint.FromCtx(c.Request.Context())
|
||||||
|
if ep == nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not found in context", nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||||
websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) {
|
websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) {
|
||||||
return HomepageItems(proto, hostname, &request), nil
|
return HomepageItems(ep, proto, hostname, &request), nil
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, HomepageItems(proto, hostname, &request))
|
c.JSON(http.StatusOK, HomepageItems(ep, proto, hostname, &request))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HomepageItems(proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
|
func HomepageItems(ep entrypoint.Entrypoint, proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
|
||||||
switch proto {
|
switch proto {
|
||||||
case "http", "https":
|
case "http", "https":
|
||||||
default:
|
default:
|
||||||
proto = "http"
|
proto = "http"
|
||||||
}
|
}
|
||||||
|
|
||||||
hp := homepage.NewHomepageMap(routes.HTTP.Size())
|
hp := homepage.NewHomepageMap(ep.HTTPRoutes().Size())
|
||||||
|
|
||||||
if strings.Count(hostname, ".") > 1 {
|
if strings.Count(hostname, ".") > 1 {
|
||||||
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
|
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range routes.HTTP.Iter {
|
for _, r := range ep.HTTPRoutes().Iter {
|
||||||
if request.Provider != "" && r.ProviderName() != request.Provider {
|
if request.Provider != "" && r.ProviderName() != request.Provider {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/route"
|
"github.com/yusing/godoxy/internal/route"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
|
|
||||||
_ "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RoutesByProvider map[string][]route.Route
|
type RoutesByProvider map[string][]route.Route
|
||||||
@@ -24,5 +24,10 @@ type RoutesByProvider map[string][]route.Route
|
|||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /route/by_provider [get]
|
// @Router /route/by_provider [get]
|
||||||
func ByProvider(c *gin.Context) {
|
func ByProvider(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, routes.ByProvider())
|
ep := entrypoint.FromCtx(c.Request.Context())
|
||||||
|
if ep == nil { // impossible, but just in case
|
||||||
|
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, ep.RoutesByProvider())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,7 +32,13 @@ func Route(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
route, ok := routes.GetIncludeExcluded(request.Which)
|
ep := entrypoint.FromCtx(c.Request.Context())
|
||||||
|
if ep == nil { // impossible, but just in case
|
||||||
|
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
route, ok := ep.GetRoute(request.Which)
|
||||||
if ok {
|
if ok {
|
||||||
c.JSON(http.StatusOK, route)
|
c.JSON(http.StatusOK, route)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/route"
|
"github.com/yusing/godoxy/internal/route"
|
||||||
"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"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
@@ -32,14 +32,16 @@ func Routes(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ep := entrypoint.FromCtx(c.Request.Context())
|
||||||
|
|
||||||
provider := c.Query("provider")
|
provider := c.Query("provider")
|
||||||
if provider == "" {
|
if provider == "" {
|
||||||
c.JSON(http.StatusOK, slices.Collect(routes.IterAll))
|
c.JSON(http.StatusOK, slices.Collect(ep.IterRoutes))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rts := make([]types.Route, 0, routes.NumAllRoutes())
|
rts := make([]types.Route, 0, ep.NumRoutes())
|
||||||
for r := range routes.IterAll {
|
for r := range ep.IterRoutes {
|
||||||
if r.ProviderName() == provider {
|
if r.ProviderName() == provider {
|
||||||
rts = append(rts, r)
|
rts = append(rts, r)
|
||||||
}
|
}
|
||||||
@@ -48,17 +50,19 @@ func Routes(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RoutesWS(c *gin.Context) {
|
func RoutesWS(c *gin.Context) {
|
||||||
|
ep := entrypoint.FromCtx(c.Request.Context())
|
||||||
|
|
||||||
provider := c.Query("provider")
|
provider := c.Query("provider")
|
||||||
if provider == "" {
|
if provider == "" {
|
||||||
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
||||||
return slices.Collect(routes.IterAll), nil
|
return slices.Collect(ep.IterRoutes), nil
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
||||||
rts := make([]types.Route, 0, routes.NumAllRoutes())
|
rts := make([]types.Route, 0, ep.NumRoutes())
|
||||||
for r := range routes.IterAll {
|
for r := range ep.IterRoutes {
|
||||||
if r.ProviderName() == provider {
|
if r.ProviderName() == provider {
|
||||||
rts = append(rts, r)
|
rts = append(rts, r)
|
||||||
}
|
}
|
||||||
|
|||||||
16
internal/autocert/types/context.go
Normal file
16
internal/autocert/types/context.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package autocert
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type ContextKey struct{}
|
||||||
|
|
||||||
|
func SetCtx(ctx interface{ SetValue(any, any) }, p Provider) {
|
||||||
|
ctx.SetValue(ContextKey{}, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromCtx(ctx context.Context) Provider {
|
||||||
|
if provider, ok := ctx.Value(ContextKey{}).(Provider); ok {
|
||||||
|
return provider
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
Setup() error
|
|
||||||
GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error)
|
GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||||
ScheduleRenewalAll(task.Parent)
|
ScheduleRenewalAll(task.Parent)
|
||||||
ObtainCertAll() error
|
ObtainCertAll() error
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ type State interface {
|
|||||||
Task() *task.Task
|
Task() *task.Task
|
||||||
Context() context.Context
|
Context() context.Context
|
||||||
Value() *Config
|
Value() *Config
|
||||||
EntrypointHandler() http.Handler
|
Entrypoint() entrypoint.Entrypoint
|
||||||
ShortLinkMatcher() config.ShortLinkMatcher
|
ShortLinkMatcher() config.ShortLinkMatcher
|
||||||
AutoCertProvider() server.CertProvider
|
AutoCertProvider() server.CertProvider
|
||||||
LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool)
|
LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool)
|
||||||
@@ -62,6 +62,12 @@ type State interface {
|
|||||||
IterProviders() iter.Seq2[string, types.RouteProvider]
|
IterProviders() iter.Seq2[string, types.RouteProvider]
|
||||||
StartProviders() error
|
StartProviders() error
|
||||||
NumProviders() int
|
NumProviders() int
|
||||||
|
|
||||||
|
// Lifecycle management
|
||||||
|
StartAPIServers()
|
||||||
|
StartMetrics()
|
||||||
|
|
||||||
|
FlushTmpLog()
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -214,12 +220,15 @@ Configuration supports hot-reloading via editing `config/config.yml`.
|
|||||||
|
|
||||||
- `internal/acl` - Access control configuration
|
- `internal/acl` - Access control configuration
|
||||||
- `internal/autocert` - SSL certificate management
|
- `internal/autocert` - SSL certificate management
|
||||||
- `internal/entrypoint` - HTTP entrypoint setup
|
- `internal/entrypoint` - HTTP entrypoint setup (now via interface)
|
||||||
- `internal/route/provider` - Route providers (Docker, file, agent)
|
- `internal/route/provider` - Route providers (Docker, file, agent)
|
||||||
- `internal/maxmind` - GeoIP configuration
|
- `internal/maxmind` - GeoIP configuration
|
||||||
- `internal/notif` - Notification providers
|
- `internal/notif` - Notification providers
|
||||||
- `internal/proxmox` - LXC container management
|
- `internal/proxmox` - LXC container management
|
||||||
- `internal/homepage/types` - Dashboard configuration
|
- `internal/homepage/types` - Dashboard configuration
|
||||||
|
- `internal/api` - REST API servers
|
||||||
|
- `internal/metrics/systeminfo` - System metrics polling
|
||||||
|
- `internal/metrics/uptime` - Uptime tracking
|
||||||
- `github.com/yusing/goutils/task` - Object lifecycle management
|
- `github.com/yusing/goutils/task` - Object lifecycle management
|
||||||
|
|
||||||
### External dependencies
|
### External dependencies
|
||||||
@@ -312,5 +321,8 @@ for name, provider := range config.GetState().IterProviders() {
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
state := config.GetState()
|
state := config.GetState()
|
||||||
http.Handle("/", state.EntrypointHandler())
|
// Get entrypoint interface for route management
|
||||||
|
ep := state.Entrypoint()
|
||||||
|
// Add routes directly to entrypoint
|
||||||
|
ep.AddRoute(route)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ import (
|
|||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
"github.com/yusing/godoxy/internal/watcher"
|
"github.com/yusing/godoxy/internal/watcher"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@@ -71,19 +69,19 @@ func Load() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// disable pool logging temporary since we already have pretty logging
|
// disable pool logging temporary since we already have pretty logging
|
||||||
routes.HTTP.DisableLog(true)
|
state.Entrypoint().DisablePoolsLog(true)
|
||||||
routes.Stream.DisableLog(true)
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
routes.HTTP.DisableLog(false)
|
state.Entrypoint().DisablePoolsLog(false)
|
||||||
routes.Stream.DisableLog(false)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
initErr := state.InitFromFile(common.ConfigPath)
|
|
||||||
err := errors.Join(initErr, state.StartProviders())
|
err := errors.Join(initErr, state.StartProviders())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logNotifyError("init", err)
|
logNotifyError("init", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.StartAPIServers()
|
||||||
|
state.StartMetrics()
|
||||||
|
|
||||||
SetState(state)
|
SetState(state)
|
||||||
|
|
||||||
// flush temporary log
|
// flush temporary log
|
||||||
@@ -118,7 +116,9 @@ func Reload() gperr.Error {
|
|||||||
logNotifyError("start providers", err)
|
logNotifyError("start providers", err)
|
||||||
return nil // continue
|
return nil // continue
|
||||||
}
|
}
|
||||||
StartProxyServers()
|
|
||||||
|
newState.StartAPIServers()
|
||||||
|
newState.StartMetrics()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,16 +152,3 @@ func OnConfigChange(ev []events.Event) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartProxyServers() {
|
|
||||||
cfg := GetState()
|
|
||||||
server.StartServer(cfg.Task(), server.Options{
|
|
||||||
Name: "proxy",
|
|
||||||
CertProvider: cfg.AutoCertProvider(),
|
|
||||||
HTTPAddr: common.ProxyHTTPAddr,
|
|
||||||
HTTPSAddr: common.ProxyHTTPSAddr,
|
|
||||||
Handler: cfg.EntrypointHandler(),
|
|
||||||
ACL: cfg.Value().ACL,
|
|
||||||
SupportProxyProtocol: cfg.Value().Entrypoint.SupportProxyProtocol,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"iter"
|
"iter"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,14 +17,20 @@ import (
|
|||||||
"github.com/goccy/go-yaml"
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/puzpuzpuz/xsync/v4"
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/yusing/godoxy/internal/acl"
|
acl "github.com/yusing/godoxy/internal/acl/types"
|
||||||
"github.com/yusing/godoxy/internal/agentpool"
|
"github.com/yusing/godoxy/internal/agentpool"
|
||||||
|
"github.com/yusing/godoxy/internal/api"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
|
autocertctx "github.com/yusing/godoxy/internal/autocert/types"
|
||||||
|
"github.com/yusing/godoxy/internal/common"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
"github.com/yusing/godoxy/internal/entrypoint"
|
"github.com/yusing/godoxy/internal/entrypoint"
|
||||||
|
entrypointctx "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
homepage "github.com/yusing/godoxy/internal/homepage/types"
|
homepage "github.com/yusing/godoxy/internal/homepage/types"
|
||||||
"github.com/yusing/godoxy/internal/logging"
|
"github.com/yusing/godoxy/internal/logging"
|
||||||
"github.com/yusing/godoxy/internal/maxmind"
|
"github.com/yusing/godoxy/internal/maxmind"
|
||||||
|
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||||
|
"github.com/yusing/godoxy/internal/metrics/uptime"
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
route "github.com/yusing/godoxy/internal/route/provider"
|
route "github.com/yusing/godoxy/internal/route/provider"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
@@ -40,7 +45,7 @@ type state struct {
|
|||||||
|
|
||||||
providers *xsync.Map[string, types.RouteProvider]
|
providers *xsync.Map[string, types.RouteProvider]
|
||||||
autocertProvider *autocert.Provider
|
autocertProvider *autocert.Provider
|
||||||
entrypoint entrypoint.Entrypoint
|
entrypoint *entrypoint.Entrypoint
|
||||||
|
|
||||||
task *task.Task
|
task *task.Task
|
||||||
|
|
||||||
@@ -65,11 +70,10 @@ func (e CriticalError) Unwrap() error {
|
|||||||
func NewState() config.State {
|
func NewState() config.State {
|
||||||
tmpLogBuf := bytes.NewBuffer(make([]byte, 0, 4096))
|
tmpLogBuf := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||||
return &state{
|
return &state{
|
||||||
providers: xsync.NewMap[string, types.RouteProvider](),
|
providers: xsync.NewMap[string, types.RouteProvider](),
|
||||||
entrypoint: entrypoint.NewEntrypoint(),
|
task: task.RootTask("config", false),
|
||||||
task: task.RootTask("config", false),
|
tmpLogBuf: tmpLogBuf,
|
||||||
tmpLogBuf: tmpLogBuf,
|
tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf),
|
||||||
tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +89,6 @@ func SetState(state config.State) {
|
|||||||
|
|
||||||
cfg := state.Value()
|
cfg := state.Value()
|
||||||
config.ActiveState.Store(state)
|
config.ActiveState.Store(state)
|
||||||
entrypoint.ActiveConfig.Store(&cfg.Entrypoint)
|
|
||||||
homepage.ActiveConfig.Store(&cfg.Homepage)
|
homepage.ActiveConfig.Store(&cfg.Homepage)
|
||||||
if autocertProvider := state.AutoCertProvider(); autocertProvider != nil {
|
if autocertProvider := state.AutoCertProvider(); autocertProvider != nil {
|
||||||
autocert.ActiveProvider.Store(autocertProvider.(*autocert.Provider))
|
autocert.ActiveProvider.Store(autocertProvider.(*autocert.Provider))
|
||||||
@@ -148,8 +151,8 @@ func (state *state) Value() *config.Config {
|
|||||||
return &state.Config
|
return &state.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *state) EntrypointHandler() http.Handler {
|
func (state *state) Entrypoint() entrypointctx.Entrypoint {
|
||||||
return &state.entrypoint
|
return state.entrypoint
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *state) ShortLinkMatcher() config.ShortLinkMatcher {
|
func (state *state) ShortLinkMatcher() config.ShortLinkMatcher {
|
||||||
@@ -204,6 +207,29 @@ func (state *state) FlushTmpLog() {
|
|||||||
state.tmpLogBuf.Reset()
|
state.tmpLogBuf.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (state *state) StartAPIServers() {
|
||||||
|
// API Handler needs to start after auth is initialized.
|
||||||
|
server.StartServer(state.task.Subtask("api_server", false), server.Options{
|
||||||
|
Name: "api",
|
||||||
|
HTTPAddr: common.APIHTTPAddr,
|
||||||
|
Handler: api.NewHandler(true),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Local API Handler is used for unauthenticated access.
|
||||||
|
if common.LocalAPIHTTPAddr != "" {
|
||||||
|
server.StartServer(state.task.Subtask("local_api_server", false), server.Options{
|
||||||
|
Name: "local_api",
|
||||||
|
HTTPAddr: common.LocalAPIHTTPAddr,
|
||||||
|
Handler: api.NewHandler(false),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) StartMetrics() {
|
||||||
|
systeminfo.Poller.Start(state.task)
|
||||||
|
uptime.Poller.Start(state.task)
|
||||||
|
}
|
||||||
|
|
||||||
// initACL initializes the ACL.
|
// initACL initializes the ACL.
|
||||||
func (state *state) initACL() error {
|
func (state *state) initACL() error {
|
||||||
if !state.ACL.Valid() {
|
if !state.ACL.Valid() {
|
||||||
@@ -213,7 +239,7 @@ func (state *state) initACL() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
state.task.SetValue(acl.ContextKey{}, state.ACL)
|
acl.SetCtx(state.task, state.ACL)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +247,7 @@ func (state *state) initEntrypoint() error {
|
|||||||
epCfg := state.Config.Entrypoint
|
epCfg := state.Config.Entrypoint
|
||||||
matchDomains := state.MatchDomains
|
matchDomains := state.MatchDomains
|
||||||
|
|
||||||
|
state.entrypoint = entrypoint.NewEntrypoint(state.task, &epCfg)
|
||||||
state.entrypoint.SetFindRouteDomains(matchDomains)
|
state.entrypoint.SetFindRouteDomains(matchDomains)
|
||||||
state.entrypoint.SetNotFoundRules(epCfg.Rules.NotFound)
|
state.entrypoint.SetNotFoundRules(epCfg.Rules.NotFound)
|
||||||
|
|
||||||
@@ -234,6 +261,8 @@ func (state *state) initEntrypoint() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entrypointctx.SetCtx(state.task, state.entrypoint)
|
||||||
|
|
||||||
errs := gperr.NewBuilder("entrypoint error")
|
errs := gperr.NewBuilder("entrypoint error")
|
||||||
errs.Add(state.entrypoint.SetMiddlewares(epCfg.Middlewares))
|
errs.Add(state.entrypoint.SetMiddlewares(epCfg.Middlewares))
|
||||||
errs.Add(state.entrypoint.SetAccessLogger(state.task, epCfg.AccessLog))
|
errs.Add(state.entrypoint.SetAccessLogger(state.task, epCfg.AccessLog))
|
||||||
@@ -310,6 +339,7 @@ func (state *state) initAutoCert() error {
|
|||||||
p.PrintCertExpiriesAll()
|
p.PrintCertExpiriesAll()
|
||||||
|
|
||||||
state.autocertProvider = p
|
state.autocertProvider = p
|
||||||
|
autocertctx.SetCtx(state.task, p)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/internal/acl"
|
"github.com/yusing/godoxy/internal/acl"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
"github.com/yusing/godoxy/internal/entrypoint"
|
||||||
homepage "github.com/yusing/godoxy/internal/homepage/types"
|
homepage "github.com/yusing/godoxy/internal/homepage/types"
|
||||||
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"iter"
|
"iter"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
"github.com/yusing/goutils/server"
|
"github.com/yusing/goutils/server"
|
||||||
"github.com/yusing/goutils/synk"
|
"github.com/yusing/goutils/synk"
|
||||||
@@ -21,7 +22,7 @@ type State interface {
|
|||||||
|
|
||||||
Value() *Config
|
Value() *Config
|
||||||
|
|
||||||
EntrypointHandler() http.Handler
|
Entrypoint() entrypoint.Entrypoint
|
||||||
ShortLinkMatcher() ShortLinkMatcher
|
ShortLinkMatcher() ShortLinkMatcher
|
||||||
AutoCertProvider() server.CertProvider
|
AutoCertProvider() server.CertProvider
|
||||||
|
|
||||||
@@ -32,6 +33,9 @@ type State interface {
|
|||||||
StartProviders() error
|
StartProviders() error
|
||||||
|
|
||||||
FlushTmpLog()
|
FlushTmpLog()
|
||||||
|
|
||||||
|
StartAPIServers()
|
||||||
|
StartMetrics()
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShortLinkMatcher interface {
|
type ShortLinkMatcher interface {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ require (
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
github.com/yusing/gointernals v0.1.16 // indirect
|
github.com/yusing/gointernals v0.1.18 // indirect
|
||||||
github.com/yusing/goutils v0.7.0 // indirect
|
github.com/yusing/goutils v0.7.0 // 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.64.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||||
|
|||||||
@@ -197,8 +197,8 @@ github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCx
|
|||||||
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
github.com/yusing/gointernals v0.1.18 h1:ou8/0tPURUgAOBJu3TN/iWF4S/5ZYQaap+rVkaJNUMw=
|
||||||
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
github.com/yusing/gointernals v0.1.18/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
|
||||||
github.com/yusing/goutils v0.7.0 h1:I5hd8GwZ+3WZqFPK0tWqek1Q5MY6Xg29hKZcwwQi4SY=
|
github.com/yusing/goutils v0.7.0 h1:I5hd8GwZ+3WZqFPK0tWqek1Q5MY6Xg29hKZcwwQi4SY=
|
||||||
github.com/yusing/goutils v0.7.0/go.mod h1:CtF/KFH4q8jkr7cvBpkaExnudE0lLu8sLe43F73Bn5Q=
|
github.com/yusing/goutils v0.7.0/go.mod h1:CtF/KFH4q8jkr7cvBpkaExnudE0lLu8sLe43F73Bn5Q=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Entrypoint
|
# Entrypoint
|
||||||
|
|
||||||
The entrypoint package provides the main HTTP entry point for GoDoxy, handling domain-based routing, middleware application, short link matching, and access logging.
|
The entrypoint package provides the main HTTP entry point for GoDoxy, handling domain-based routing, middleware application, short link matching, access logging, and HTTP server lifecycle management.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The entrypoint package implements the primary HTTP handler that receives all incoming requests, determines the target route based on hostname, applies middleware, and forwards requests to the appropriate route handler.
|
The entrypoint package implements the primary HTTP handler that receives all incoming requests, manages the lifecycle of HTTP servers, determines the target route based on hostname, applies middleware, and forwards requests to the appropriate route handler.
|
||||||
|
|
||||||
### Key Features
|
### Key Features
|
||||||
|
|
||||||
@@ -14,103 +14,350 @@ The entrypoint package implements the primary HTTP handler that receives all inc
|
|||||||
- Access logging for all requests
|
- Access logging for all requests
|
||||||
- Configurable not-found handling
|
- Configurable not-found handling
|
||||||
- Per-domain route resolution
|
- Per-domain route resolution
|
||||||
|
- HTTP server management (HTTP/HTTPS)
|
||||||
|
- Route pool abstractions via [`PoolLike`](internal/entrypoint/types/entrypoint.go:27) and [`RWPoolLike`](internal/entrypoint/types/entrypoint.go:33) interfaces
|
||||||
|
|
||||||
## Architecture
|
### Primary Consumers
|
||||||
|
|
||||||
```mermaid
|
- **HTTP servers**: Per-listen-addr servers dispatch requests to routes
|
||||||
graph TD
|
- **Route providers**: Register routes via [`StartAddRoute`](internal/entrypoint/routes.go:48)
|
||||||
A[HTTP Request] --> B[Entrypoint Handler]
|
- **Configuration layer**: Validates and applies middleware/access-logging config
|
||||||
B --> C{Access Logger?}
|
|
||||||
C -->|Yes| D[Wrap Response Recorder]
|
|
||||||
C -->|No| E[Skip Logging]
|
|
||||||
|
|
||||||
D --> F[Find Route by Host]
|
### Non-goals
|
||||||
E --> F
|
|
||||||
|
|
||||||
F --> G{Route Found?}
|
- Does not implement route discovery (delegates to providers)
|
||||||
G -->|Yes| H{Middleware?}
|
- Does not handle TLS certificate management (delegates to autocert)
|
||||||
G -->|No| I{Short Link?}
|
- Does not implement health checks (delegates to `internal/health/monitor`)
|
||||||
I -->|Yes| J[Short Link Handler]
|
- Does not manage TCP/UDP listeners directly (only HTTP/HTTPS via `goutils/server`)
|
||||||
I -->|No| K{Not Found Handler?}
|
|
||||||
K -->|Yes| L[Not Found Handler]
|
|
||||||
K -->|No| M[Serve 404]
|
|
||||||
|
|
||||||
H -->|Yes| N[Apply Middleware]
|
### Stability
|
||||||
H -->|No| O[Direct Route]
|
|
||||||
N --> O
|
|
||||||
|
|
||||||
O --> P[Route ServeHTTP]
|
Internal package with stable core interfaces. The [`Entrypoint`](internal/entrypoint/types/entrypoint.go:7) interface is the public contract.
|
||||||
P --> Q[Response]
|
|
||||||
|
|
||||||
L --> R[404 Response]
|
|
||||||
J --> Q
|
|
||||||
M --> R
|
|
||||||
```
|
|
||||||
|
|
||||||
## Core Components
|
|
||||||
|
|
||||||
### Entrypoint Structure
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Entrypoint struct {
|
|
||||||
middleware *middleware.Middleware
|
|
||||||
notFoundHandler http.Handler
|
|
||||||
accessLogger accesslog.AccessLogger
|
|
||||||
findRouteFunc func(host string) types.HTTPRoute
|
|
||||||
shortLinkTree *ShortLinkMatcher
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Active Config
|
|
||||||
|
|
||||||
```go
|
|
||||||
var ActiveConfig atomic.Pointer[entrypoint.Config]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Public API
|
## Public API
|
||||||
|
|
||||||
### Creation
|
### Entrypoint Interface
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// NewEntrypoint creates a new entrypoint instance.
|
type Entrypoint interface {
|
||||||
func NewEntrypoint() Entrypoint
|
// Server capabilities
|
||||||
|
SupportProxyProtocol() bool
|
||||||
|
DisablePoolsLog(v bool)
|
||||||
|
|
||||||
|
// Route registry access
|
||||||
|
GetRoute(alias string) (types.Route, bool)
|
||||||
|
StartAddRoute(r types.Route) error
|
||||||
|
IterRoutes(yield func(r types.Route) bool)
|
||||||
|
NumRoutes() int
|
||||||
|
RoutesByProvider() map[string][]types.Route
|
||||||
|
|
||||||
|
// Route pool accessors
|
||||||
|
HTTPRoutes() PoolLike[types.HTTPRoute]
|
||||||
|
StreamRoutes() PoolLike[types.StreamRoute]
|
||||||
|
ExcludedRoutes() RWPoolLike[types.Route]
|
||||||
|
|
||||||
|
// Health info queries
|
||||||
|
GetHealthInfo() map[string]types.HealthInfo
|
||||||
|
GetHealthInfoWithoutDetail() map[string]types.HealthInfoWithoutDetail
|
||||||
|
GetHealthInfoSimple() map[string]types.HealthStatus
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
SetFindRouteDomains(domains []string)
|
||||||
|
SetMiddlewares(mws []map[string]any) error
|
||||||
|
SetNotFoundRules(rules rules.Rules)
|
||||||
|
SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) error
|
||||||
|
|
||||||
|
// Context integration
|
||||||
|
ShortLinkMatcher() *ShortLinkMatcher
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pool Interfaces
|
||||||
|
|
||||||
|
```go
|
||||||
|
type PoolLike[Route types.Route] interface {
|
||||||
|
Get(alias string) (Route, bool)
|
||||||
|
Iter(yield func(alias string, r Route) bool)
|
||||||
|
Size() int
|
||||||
|
}
|
||||||
|
|
||||||
|
type RWPoolLike[Route types.Route] interface {
|
||||||
|
PoolLike[Route]
|
||||||
|
Add(r Route)
|
||||||
|
Del(r Route)
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// SetFindRouteDomains configures domain-based route lookup.
|
type Config struct {
|
||||||
func (ep *Entrypoint) SetFindRouteDomains(domains []string)
|
SupportProxyProtocol bool `json:"support_proxy_protocol"`
|
||||||
|
Rules struct {
|
||||||
// SetMiddlewares loads and configures middleware chain.
|
NotFound rules.Rules `json:"not_found"`
|
||||||
func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error
|
} `json:"rules"`
|
||||||
|
Middlewares []map[string]any `json:"middlewares"`
|
||||||
// SetNotFoundRules configures the not-found handler.
|
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
|
||||||
func (ep *Entrypoint) SetNotFoundRules(rules rules.Rules)
|
}
|
||||||
|
|
||||||
// SetAccessLogger initializes access logging.
|
|
||||||
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) error
|
|
||||||
|
|
||||||
// ShortLinkMatcher returns the short link matcher.
|
|
||||||
func (ep *Entrypoint) ShortLinkMatcher() *ShortLinkMatcher
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Request Handling
|
### Context Functions
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// ServeHTTP is the main HTTP handler.
|
func SetCtx(ctx interface{ SetValue(any, any) }, ep Entrypoint)
|
||||||
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
func FromCtx(ctx context.Context) Entrypoint
|
||||||
|
|
||||||
// FindRoute looks up a route by hostname.
|
|
||||||
func (ep *Entrypoint) FindRoute(s string) types.HTTPRoute
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class Entrypoint {
|
||||||
|
+task *task.new_task
|
||||||
|
+cfg *Config
|
||||||
|
+middleware *middleware.Middleware
|
||||||
|
+notFoundHandler http.Handler
|
||||||
|
+accessLogger AccessLogger
|
||||||
|
+findRouteFunc findRouteFunc
|
||||||
|
+shortLinkMatcher *ShortLinkMatcher
|
||||||
|
+streamRoutes *pool.Pool[types.StreamRoute]
|
||||||
|
+excludedRoutes *pool.Pool[types.Route]
|
||||||
|
+servers *xsync.Map[string, *httpServer]
|
||||||
|
+SupportProxyProtocol() bool
|
||||||
|
+StartAddRoute(r) error
|
||||||
|
+IterRoutes(yield)
|
||||||
|
+HTTPRoutes() PoolLike
|
||||||
|
}
|
||||||
|
|
||||||
|
class httpServer {
|
||||||
|
+routes *pool.Pool[types.HTTPRoute]
|
||||||
|
+ServeHTTP(w, r)
|
||||||
|
+AddRoute(route)
|
||||||
|
+DelRoute(route)
|
||||||
|
+FindRoute(s) types.HTTPRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
class PoolLike {
|
||||||
|
<<interface>>
|
||||||
|
+Get(alias) (Route, bool)
|
||||||
|
+Iter(yield) bool
|
||||||
|
+Size() int
|
||||||
|
}
|
||||||
|
|
||||||
|
class RWPoolLike {
|
||||||
|
<<interface>>
|
||||||
|
+PoolLike
|
||||||
|
+Add(r Route)
|
||||||
|
+Del(r Route)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShortLinkMatcher {
|
||||||
|
+fqdnRoutes *xsync.Map[string, string]
|
||||||
|
+subdomainRoutes *xsync.Map[string, struct{}]
|
||||||
|
+ServeHTTP(w, r)
|
||||||
|
+AddRoute(alias)
|
||||||
|
+DelRoute(alias)
|
||||||
|
+SetDefaultDomainSuffix(suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
Entrypoint --> httpServer : manages
|
||||||
|
Entrypoint --> ShortLinkMatcher : owns
|
||||||
|
Entrypoint --> PoolLike : HTTPRoutes()
|
||||||
|
Entrypoint --> RWPoolLike : ExcludedRoutes()
|
||||||
|
httpServer --> PoolLike : routes pool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request Processing Pipeline
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[HTTP Request] --> B[Find Route by Host]
|
||||||
|
B --> C{Route Found?}
|
||||||
|
C -->|Yes| D{Middleware?}
|
||||||
|
C -->|No| E{Short Link?}
|
||||||
|
E -->|Yes| F[Short Link Handler]
|
||||||
|
E -->|No| G{Not Found Handler?}
|
||||||
|
G -->|Yes| H[Not Found Handler]
|
||||||
|
G -->|No| I[Serve 404]
|
||||||
|
|
||||||
|
D -->|Yes| J[Apply Middleware Chain]
|
||||||
|
D -->|No| K[Direct Route Handler]
|
||||||
|
J --> K
|
||||||
|
|
||||||
|
K --> L[Route ServeHTTP]
|
||||||
|
L --> M[Response]
|
||||||
|
|
||||||
|
F --> M
|
||||||
|
H --> N[404 Response]
|
||||||
|
I --> N
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Lifecycle
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Empty: NewEntrypoint()
|
||||||
|
|
||||||
|
Empty --> Listening: StartAddRoute()
|
||||||
|
Listening --> Listening: StartAddRoute()
|
||||||
|
Listening --> Listening: delHTTPRoute()
|
||||||
|
Listening --> [*]: Cancel()
|
||||||
|
|
||||||
|
Listening --> AddingServer: addHTTPRoute()
|
||||||
|
AddingServer --> Listening: Server starts
|
||||||
|
|
||||||
|
note right of Listening
|
||||||
|
servers map: addr -> httpServer
|
||||||
|
For HTTPS, routes are added to ProxyHTTPSAddr
|
||||||
|
Default routes added to both HTTP and HTTPS
|
||||||
|
end note
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant httpServer
|
||||||
|
participant Entrypoint
|
||||||
|
participant Middleware
|
||||||
|
participant Route
|
||||||
|
|
||||||
|
Client->>httpServer: GET /path
|
||||||
|
httpServer->>Entrypoint: FindRoute(host)
|
||||||
|
|
||||||
|
alt Route Found
|
||||||
|
Entrypoint-->>httpServer: HTTPRoute
|
||||||
|
httpServer->>Middleware: ServeHTTP(routeHandler)
|
||||||
|
alt Has Middleware
|
||||||
|
Middleware->>Middleware: Process Chain
|
||||||
|
end
|
||||||
|
Middleware->>Route: Forward Request
|
||||||
|
Route-->>Middleware: Response
|
||||||
|
Middleware-->>httpServer: Response
|
||||||
|
else Short Link (go.example.com/alias)
|
||||||
|
httpServer->>ShortLinkMatcher: Match short code
|
||||||
|
ShortLinkMatcher-->>httpServer: Redirect
|
||||||
|
else Not Found
|
||||||
|
httpServer->>NotFoundHandler: Serve 404
|
||||||
|
NotFoundHandler-->>httpServer: 404 Page
|
||||||
|
end
|
||||||
|
|
||||||
|
httpServer-->>Client: Response
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route Registry
|
||||||
|
|
||||||
|
Routes are managed per-entrypoint:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Adding a route (main entry point for providers)
|
||||||
|
if err := ep.StartAddRoute(route); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterating all routes including excluded
|
||||||
|
ep.IterRoutes(func(r types.Route) bool {
|
||||||
|
log.Info().Str("alias", r.Name()).Msg("route")
|
||||||
|
return true // continue iteration
|
||||||
|
})
|
||||||
|
|
||||||
|
// Querying by alias
|
||||||
|
route, ok := ep.GetRoute("myapp")
|
||||||
|
|
||||||
|
// Grouping by provider
|
||||||
|
byProvider := ep.RoutesByProvider()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Surface
|
||||||
|
|
||||||
|
### Config Source
|
||||||
|
|
||||||
|
Environment variables and YAML config file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
entrypoint:
|
||||||
|
support_proxy_protocol: true
|
||||||
|
middlewares:
|
||||||
|
- rate_limit:
|
||||||
|
requests_per_second: 100
|
||||||
|
rules:
|
||||||
|
not_found:
|
||||||
|
# not-found rules configuration
|
||||||
|
access_log:
|
||||||
|
path: /var/log/godoxy/access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| ------------------------------ | ----------------------------- |
|
||||||
|
| `PROXY_SUPPORT_PROXY_PROTOCOL` | Enable PROXY protocol support |
|
||||||
|
|
||||||
|
## Dependency and Integration Map
|
||||||
|
|
||||||
|
| Dependency | Purpose |
|
||||||
|
| ---------------------------------- | --------------------------- |
|
||||||
|
| `internal/route` | Route types and handlers |
|
||||||
|
| `internal/route/rules` | Not-found rules processing |
|
||||||
|
| `internal/logging/accesslog` | Request logging |
|
||||||
|
| `internal/net/gphttp/middleware` | Middleware chain |
|
||||||
|
| `internal/types` | Route and health types |
|
||||||
|
| `github.com/puzpuzpuz/xsync/v4` | Concurrent server map |
|
||||||
|
| `github.com/yusing/goutils/pool` | Route pool implementations |
|
||||||
|
| `github.com/yusing/goutils/task` | Lifecycle management |
|
||||||
|
| `github.com/yusing/goutils/server` | HTTP/HTTPS server lifecycle |
|
||||||
|
|
||||||
|
## Observability
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
| Level | Context | Description |
|
||||||
|
| ------- | --------------------- | ----------------------- |
|
||||||
|
| `DEBUG` | `route`, `listen_url` | Route addition/removal |
|
||||||
|
| `DEBUG` | `addr`, `proto` | Server lifecycle |
|
||||||
|
| `ERROR` | `route`, `listen_url` | Server startup failures |
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
Route metrics exposed via [`GetHealthInfo`](internal/entrypoint/query.go:10) methods:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Health info for all routes
|
||||||
|
healthMap := ep.GetHealthInfo()
|
||||||
|
// {
|
||||||
|
// "myapp": {Status: "healthy", Uptime: 3600, Latency: 5ms},
|
||||||
|
// "excluded-route": {Status: "unknown", Detail: "n/a"},
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Route lookup is read-only from route pools
|
||||||
|
- Middleware chain is applied per-request
|
||||||
|
- Proxy protocol support must be explicitly enabled
|
||||||
|
- Access logger captures request metadata before processing
|
||||||
|
- Short link matching is limited to configured domains
|
||||||
|
|
||||||
|
## Failure Modes and Recovery
|
||||||
|
|
||||||
|
| Failure | Behavior | Recovery |
|
||||||
|
| --------------------- | ------------------------------- | ---------------------------- |
|
||||||
|
| Server bind fails | Error returned, route not added | Fix port/address conflict |
|
||||||
|
| Route start fails | Route excluded, error logged | Fix route configuration |
|
||||||
|
| Middleware load fails | SetMiddlewares returns error | Fix middleware configuration |
|
||||||
|
| Context cancelled | All servers stopped gracefully | Restart entrypoint |
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
### Basic Setup
|
### Basic Setup
|
||||||
|
|
||||||
```go
|
```go
|
||||||
ep := entrypoint.NewEntrypoint()
|
ep := entrypoint.NewEntrypoint(parent, &entrypoint.Config{
|
||||||
|
SupportProxyProtocol: false,
|
||||||
|
})
|
||||||
|
|
||||||
// Configure domain matching
|
// Configure domain matching
|
||||||
ep.SetFindRouteDomains([]string{".example.com", "example.com"})
|
ep.SetFindRouteDomains([]string{".example.com", "example.com"})
|
||||||
@@ -120,7 +367,7 @@ err := ep.SetMiddlewares([]map[string]any{
|
|||||||
{"rate_limit": map[string]any{"requests_per_second": 100}},
|
{"rate_limit": map[string]any{"requests_per_second": 100}},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure access logging
|
// Configure access logging
|
||||||
@@ -128,181 +375,58 @@ err = ep.SetAccessLogger(parent, &accesslog.RequestLoggerConfig{
|
|||||||
Path: "/var/log/godoxy/access.log",
|
Path: "/var/log/godoxy/access.log",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start server
|
|
||||||
http.ListenAndServe(":80", &ep)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Route Lookup Logic
|
### Route Querying
|
||||||
|
|
||||||
The entrypoint uses multiple strategies to find routes:
|
|
||||||
|
|
||||||
1. **Subdomain Matching**: For `sub.domain.com`, looks for `sub`
|
|
||||||
1. **Exact Match**: Looks for the full hostname
|
|
||||||
1. **Port Stripping**: Strips port from host if present
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func findRouteAnyDomain(host string) types.HTTPRoute {
|
// Iterate all routes including excluded
|
||||||
// Try subdomain (everything before first dot)
|
ep.IterRoutes(func(r types.Route) bool {
|
||||||
idx := strings.IndexByte(host, '.')
|
log.Info().
|
||||||
if idx != -1 {
|
Str("alias", r.Name()).
|
||||||
target := host[:idx]
|
Str("provider", r.ProviderName()).
|
||||||
if r, ok := routes.HTTP.Get(target); ok {
|
Bool("excluded", r.ShouldExclude()).
|
||||||
return r
|
Msg("route")
|
||||||
}
|
return true // continue iteration
|
||||||
}
|
})
|
||||||
|
|
||||||
// Try exact match
|
// Get health info for all routes
|
||||||
if r, ok := routes.HTTP.Get(host); ok {
|
healthMap := ep.GetHealthInfoSimple()
|
||||||
return r
|
for alias, status := range healthMap {
|
||||||
}
|
log.Info().Str("alias", alias).Str("status", string(status)).Msg("health")
|
||||||
|
|
||||||
// Try stripping port
|
|
||||||
if before, _, ok := strings.Cut(host, ":"); ok {
|
|
||||||
if r, ok := routes.HTTP.Get(before); ok {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Short Links
|
### Route Addition
|
||||||
|
|
||||||
Short links use a special `.short` domain:
|
Routes are typically added by providers via `StartAddRoute`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Request to: https://abc.short.example.com
|
// StartAddRoute handles route registration and server creation
|
||||||
// Looks for route with alias "abc"
|
if err := ep.StartAddRoute(route); err != nil {
|
||||||
if strings.EqualFold(host, common.ShortLinkPrefix) {
|
return err
|
||||||
// Handle short link
|
|
||||||
ep.shortLinkTree.ServeHTTP(w, r)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data Flow
|
### Context Integration
|
||||||
|
|
||||||
```mermaid
|
Routes can access the entrypoint from request context:
|
||||||
sequenceDiagram
|
|
||||||
participant Client
|
|
||||||
participant Entrypoint
|
|
||||||
participant Middleware
|
|
||||||
participant Route
|
|
||||||
participant Logger
|
|
||||||
|
|
||||||
Client->>Entrypoint: GET /path
|
|
||||||
Entrypoint->>Entrypoint: FindRoute(host)
|
|
||||||
alt Route Found
|
|
||||||
Entrypoint->>Logger: Get ResponseRecorder
|
|
||||||
Logger-->>Entrypoint: Recorder
|
|
||||||
Entrypoint->>Middleware: ServeHTTP(routeHandler)
|
|
||||||
alt Has Middleware
|
|
||||||
Middleware->>Middleware: Process Chain
|
|
||||||
end
|
|
||||||
Middleware->>Route: Forward Request
|
|
||||||
Route-->>Middleware: Response
|
|
||||||
Middleware-->>Entrypoint: Response
|
|
||||||
else Short Link
|
|
||||||
Entrypoint->>ShortLinkTree: Match short code
|
|
||||||
ShortLinkTree-->>Entrypoint: Redirect
|
|
||||||
else Not Found
|
|
||||||
Entrypoint->>NotFoundHandler: Serve 404
|
|
||||||
NotFoundHandler-->>Entrypoint: 404 Page
|
|
||||||
end
|
|
||||||
|
|
||||||
Entrypoint->>Logger: Log Request
|
|
||||||
Logger-->>Entrypoint: Complete
|
|
||||||
Entrypoint-->>Client: Response
|
|
||||||
```
|
|
||||||
|
|
||||||
## Not-Found Handling
|
|
||||||
|
|
||||||
When no route is found, the entrypoint:
|
|
||||||
|
|
||||||
1. Attempts to serve a static error page file
|
|
||||||
1. Logs the 404 request
|
|
||||||
1. Falls back to the configured error page
|
|
||||||
1. Returns 404 status code
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (ep *Entrypoint) serveNotFound(w http.ResponseWriter, r *http.Request) {
|
// Set entrypoint in context (typically during initialization)
|
||||||
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
|
entrypoint.SetCtx(task, ep)
|
||||||
log.Error().
|
|
||||||
Str("method", r.Method).
|
|
||||||
Str("url", r.URL.String()).
|
|
||||||
Str("remote", r.RemoteAddr).
|
|
||||||
Msgf("not found: %s", r.Host)
|
|
||||||
|
|
||||||
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
|
// Get entrypoint from context
|
||||||
if ok {
|
if ep := entrypoint.FromCtx(r.Context()); ep != nil {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
route, ok := ep.GetRoute("alias")
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
w.Write(errorPage)
|
|
||||||
} else {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration Structure
|
## Testing Notes
|
||||||
|
|
||||||
```go
|
- Benchmark tests in [`entrypoint_benchmark_test.go`](internal/entrypoint/entrypoint_benchmark_test.go)
|
||||||
type Config struct {
|
- Integration tests in [`entrypoint_test.go`](internal/entrypoint/entrypoint_test.go)
|
||||||
Middlewares []map[string]any `json:"middlewares"`
|
- Mock route pools for unit testing
|
||||||
Rules rules.Rules `json:"rules"`
|
- Short link tests in [`shortlink_test.go`](internal/entrypoint/shortlink_test.go)
|
||||||
AccessLog *accesslog.RequestLoggerConfig `json:"access_log"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Middleware Integration
|
|
||||||
|
|
||||||
The entrypoint supports middleware chains configured via YAML:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
entrypoint:
|
|
||||||
middlewares:
|
|
||||||
- use: rate_limit
|
|
||||||
average: 100
|
|
||||||
burst: 200
|
|
||||||
bypass:
|
|
||||||
- remote 192.168.1.0/24
|
|
||||||
- use: redirect_http
|
|
||||||
```
|
|
||||||
|
|
||||||
## Access Logging
|
|
||||||
|
|
||||||
Access logging wraps the response recorder to capture:
|
|
||||||
|
|
||||||
- Request method and URL
|
|
||||||
- Response status code
|
|
||||||
- Response size
|
|
||||||
- Request duration
|
|
||||||
- Client IP address
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if ep.accessLogger != nil {
|
|
||||||
rec := accesslog.GetResponseRecorder(w)
|
|
||||||
w = rec
|
|
||||||
defer func() {
|
|
||||||
ep.accessLogger.Log(r, rec.Response())
|
|
||||||
accesslog.PutResponseRecorder(rec)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
// ... handle request
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration Points
|
|
||||||
|
|
||||||
The entrypoint integrates with:
|
|
||||||
|
|
||||||
- **Route Registry**: HTTP route lookup
|
|
||||||
- **Middleware**: Request processing chain
|
|
||||||
- **AccessLog**: Request logging
|
|
||||||
- **ErrorPage**: 404 error pages
|
|
||||||
- **ShortLink**: Short link handling
|
|
||||||
|
|||||||
@@ -4,46 +4,114 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
|
||||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware/errorpage"
|
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
"github.com/yusing/godoxy/internal/route/rules"
|
"github.com/yusing/godoxy/internal/route/rules"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
"github.com/yusing/goutils/pool"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type HTTPRoutes interface {
|
||||||
|
Get(alias string) (types.HTTPRoute, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
type findRouteFunc func(HTTPRoutes, string) types.HTTPRoute
|
||||||
|
|
||||||
type Entrypoint struct {
|
type Entrypoint struct {
|
||||||
middleware *middleware.Middleware
|
task *task.Task
|
||||||
notFoundHandler http.Handler
|
|
||||||
accessLogger accesslog.AccessLogger
|
cfg *Config
|
||||||
findRouteFunc func(host string) types.HTTPRoute
|
|
||||||
shortLinkMatcher *ShortLinkMatcher
|
middleware *middleware.Middleware
|
||||||
|
notFoundHandler http.Handler
|
||||||
|
accessLogger accesslog.AccessLogger
|
||||||
|
findRouteFunc findRouteFunc
|
||||||
|
shortLinkMatcher *ShortLinkMatcher
|
||||||
|
|
||||||
|
streamRoutes *pool.Pool[types.StreamRoute]
|
||||||
|
excludedRoutes *pool.Pool[types.Route]
|
||||||
|
|
||||||
|
// this only affects future http servers creation
|
||||||
|
httpPoolDisableLog atomic.Bool
|
||||||
|
|
||||||
|
servers *xsync.Map[string, *httpServer] // listen addr -> server
|
||||||
}
|
}
|
||||||
|
|
||||||
// nil-safe
|
var _ entrypoint.Entrypoint = &Entrypoint{}
|
||||||
var ActiveConfig atomic.Pointer[entrypoint.Config]
|
|
||||||
|
|
||||||
func init() {
|
var emptyCfg Config
|
||||||
// make sure it's not nil
|
|
||||||
ActiveConfig.Store(&entrypoint.Config{})
|
func NewTestEntrypoint(t testing.TB, cfg *Config) *Entrypoint {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
testTask := task.GetTestTask(t)
|
||||||
|
ep := NewEntrypoint(testTask, cfg)
|
||||||
|
entrypoint.SetCtx(testTask, ep)
|
||||||
|
return ep
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEntrypoint() Entrypoint {
|
func NewEntrypoint(parent task.Parent, cfg *Config) *Entrypoint {
|
||||||
return Entrypoint{
|
if cfg == nil {
|
||||||
findRouteFunc: findRouteAnyDomain,
|
cfg = &emptyCfg
|
||||||
shortLinkMatcher: newShortLinkMatcher(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ep := &Entrypoint{
|
||||||
|
task: parent.Subtask("entrypoint", false),
|
||||||
|
cfg: cfg,
|
||||||
|
findRouteFunc: findRouteAnyDomain,
|
||||||
|
shortLinkMatcher: newShortLinkMatcher(),
|
||||||
|
streamRoutes: pool.New[types.StreamRoute]("stream_routes"),
|
||||||
|
excludedRoutes: pool.New[types.Route]("excluded_routes"),
|
||||||
|
servers: xsync.NewMap[string, *httpServer](),
|
||||||
|
}
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) Task() *task.Task {
|
||||||
|
return ep.task
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) SupportProxyProtocol() bool {
|
||||||
|
return ep.cfg.SupportProxyProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) DisablePoolsLog(v bool) {
|
||||||
|
ep.httpPoolDisableLog.Store(v)
|
||||||
|
// apply to all running http servers
|
||||||
|
for _, srv := range ep.servers.Range {
|
||||||
|
srv.routes.DisableLog(v)
|
||||||
|
}
|
||||||
|
// apply to other pools
|
||||||
|
ep.streamRoutes.DisableLog(v)
|
||||||
|
ep.excludedRoutes.DisableLog(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ep *Entrypoint) ShortLinkMatcher() *ShortLinkMatcher {
|
func (ep *Entrypoint) ShortLinkMatcher() *ShortLinkMatcher {
|
||||||
return ep.shortLinkMatcher
|
return ep.shortLinkMatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) HTTPRoutes() entrypoint.PoolLike[types.HTTPRoute] {
|
||||||
|
return newHTTPPoolAdapter(ep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) StreamRoutes() entrypoint.PoolLike[types.StreamRoute] {
|
||||||
|
return ep.streamRoutes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) ExcludedRoutes() entrypoint.RWPoolLike[types.Route] {
|
||||||
|
return ep.excludedRoutes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) GetServer(addr string) (HTTPServer, bool) {
|
||||||
|
return ep.servers.Load(addr)
|
||||||
|
}
|
||||||
|
|
||||||
func (ep *Entrypoint) SetFindRouteDomains(domains []string) {
|
func (ep *Entrypoint) SetFindRouteDomains(domains []string) {
|
||||||
if len(domains) == 0 {
|
if len(domains) == 0 {
|
||||||
ep.findRouteFunc = findRouteAnyDomain
|
ep.findRouteFunc = findRouteAnyDomain
|
||||||
@@ -74,7 +142,7 @@ func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ep *Entrypoint) SetNotFoundRules(rules rules.Rules) {
|
func (ep *Entrypoint) SetNotFoundRules(rules rules.Rules) {
|
||||||
ep.notFoundHandler = rules.BuildHandler(http.HandlerFunc(ep.serveNotFound))
|
ep.notFoundHandler = rules.BuildHandler(serveNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) (err error) {
|
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) (err error) {
|
||||||
@@ -91,111 +159,39 @@ func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.Request
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ep *Entrypoint) FindRoute(s string) types.HTTPRoute {
|
func findRouteAnyDomain(routes HTTPRoutes, host string) types.HTTPRoute {
|
||||||
return ep.findRouteFunc(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if ep.accessLogger != nil {
|
|
||||||
rec := accesslog.GetResponseRecorder(w)
|
|
||||||
w = rec
|
|
||||||
defer func() {
|
|
||||||
ep.accessLogger.LogRequest(r, rec.Response())
|
|
||||||
accesslog.PutResponseRecorder(rec)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
route := ep.findRouteFunc(r.Host)
|
|
||||||
switch {
|
|
||||||
case route != nil:
|
|
||||||
r = routes.WithRouteContext(r, route)
|
|
||||||
if ep.middleware != nil {
|
|
||||||
ep.middleware.ServeHTTP(route.ServeHTTP, w, r)
|
|
||||||
} else {
|
|
||||||
route.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
case ep.tryHandleShortLink(w, r):
|
|
||||||
return
|
|
||||||
case ep.notFoundHandler != nil:
|
|
||||||
ep.notFoundHandler.ServeHTTP(w, r)
|
|
||||||
default:
|
|
||||||
ep.serveNotFound(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ep *Entrypoint) tryHandleShortLink(w http.ResponseWriter, r *http.Request) (handled bool) {
|
|
||||||
host := r.Host
|
|
||||||
if before, _, ok := strings.Cut(host, ":"); ok {
|
|
||||||
host = before
|
|
||||||
}
|
|
||||||
if strings.EqualFold(host, common.ShortLinkPrefix) {
|
|
||||||
if ep.middleware != nil {
|
|
||||||
ep.middleware.ServeHTTP(ep.shortLinkMatcher.ServeHTTP, w, r)
|
|
||||||
} else {
|
|
||||||
ep.shortLinkMatcher.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ep *Entrypoint) serveNotFound(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Why use StatusNotFound instead of StatusBadRequest or StatusBadGateway?
|
|
||||||
// On nginx, when route for domain does not exist, it returns StatusBadGateway.
|
|
||||||
// Then scraper / scanners will know the subdomain is invalid.
|
|
||||||
// With StatusNotFound, they won't know whether it's the path, or the subdomain that is invalid.
|
|
||||||
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
|
|
||||||
log.Error().
|
|
||||||
Str("method", r.Method).
|
|
||||||
Str("url", r.URL.String()).
|
|
||||||
Str("remote", r.RemoteAddr).
|
|
||||||
Msgf("not found: %s", r.Host)
|
|
||||||
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
|
|
||||||
if ok {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
if _, err := w.Write(errorPage); err != nil {
|
|
||||||
log.Err(err).Msg("failed to write error page")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func findRouteAnyDomain(host string) types.HTTPRoute {
|
|
||||||
idx := strings.IndexByte(host, '.')
|
idx := strings.IndexByte(host, '.')
|
||||||
if idx != -1 {
|
if idx != -1 {
|
||||||
target := host[:idx]
|
target := host[:idx]
|
||||||
if r, ok := routes.HTTP.Get(target); ok {
|
if r, ok := routes.Get(target); ok {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r, ok := routes.HTTP.Get(host); ok {
|
if r, ok := routes.Get(host); ok {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
// try striping the trailing :port from the host
|
// try striping the trailing :port from the host
|
||||||
if before, _, ok := strings.Cut(host, ":"); ok {
|
if before, _, ok := strings.Cut(host, ":"); ok {
|
||||||
if r, ok := routes.HTTP.Get(before); ok {
|
if r, ok := routes.Get(before); ok {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findRouteByDomains(domains []string) func(host string) types.HTTPRoute {
|
func findRouteByDomains(domains []string) func(routes HTTPRoutes, host string) types.HTTPRoute {
|
||||||
return func(host string) types.HTTPRoute {
|
return func(routes HTTPRoutes, host string) types.HTTPRoute {
|
||||||
host, _, _ = strings.Cut(host, ":") // strip the trailing :port
|
host, _, _ = strings.Cut(host, ":") // strip the trailing :port
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
if target, ok := strings.CutSuffix(host, domain); ok {
|
if target, ok := strings.CutSuffix(host, domain); ok {
|
||||||
if r, ok := routes.HTTP.Get(target); ok {
|
if r, ok := routes.Get(target); ok {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback to exact match
|
// fallback to exact match
|
||||||
if r, ok := routes.HTTP.Get(host); ok {
|
if r, ok := routes.Get(host); ok {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/internal/common"
|
||||||
. "github.com/yusing/godoxy/internal/entrypoint"
|
. "github.com/yusing/godoxy/internal/entrypoint"
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/route"
|
"github.com/yusing/godoxy/internal/route"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
routeTypes "github.com/yusing/godoxy/internal/route/types"
|
routeTypes "github.com/yusing/godoxy/internal/route/types"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
@@ -48,13 +50,15 @@ func (t noopTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkEntrypointReal(b *testing.B) {
|
func BenchmarkEntrypointReal(b *testing.B) {
|
||||||
var ep Entrypoint
|
task := task.GetTestTask(b)
|
||||||
|
ep := NewEntrypoint(task, nil)
|
||||||
req := http.Request{
|
req := http.Request{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
URL: &url.URL{Path: "/", RawPath: "/"},
|
URL: &url.URL{Path: "/", RawPath: "/"},
|
||||||
Host: "test.domain.tld",
|
Host: "test.domain.tld",
|
||||||
}
|
}
|
||||||
ep.SetFindRouteDomains([]string{})
|
ep.SetFindRouteDomains([]string{})
|
||||||
|
entrypoint.SetCtx(task, ep)
|
||||||
|
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Length", "1")
|
w.Header().Set("Content-Length", "1")
|
||||||
@@ -77,48 +81,48 @@ func BenchmarkEntrypointReal(b *testing.B) {
|
|||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &route.Route{
|
r, err := route.NewStartedTestRoute(b, &route.Route{
|
||||||
Alias: "test",
|
Alias: "test",
|
||||||
Scheme: routeTypes.SchemeHTTP,
|
Scheme: routeTypes.SchemeHTTP,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: route.Port{Proxy: portInt},
|
Port: route.Port{Proxy: portInt},
|
||||||
HealthCheck: types.HealthCheckConfig{Disable: true},
|
HealthCheck: types.HealthCheckConfig{Disable: true},
|
||||||
}
|
})
|
||||||
|
|
||||||
err = r.Validate()
|
require.NoError(b, err)
|
||||||
if err != nil {
|
require.False(b, r.ShouldExclude())
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.Start(task.RootTask("test", false))
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var w noopResponseWriter
|
var w noopResponseWriter
|
||||||
|
|
||||||
|
server, ok := ep.GetServer(common.ProxyHTTPAddr)
|
||||||
|
if !ok {
|
||||||
|
b.Fatal("server not found")
|
||||||
|
}
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for b.Loop() {
|
for b.Loop() {
|
||||||
ep.ServeHTTP(&w, &req)
|
server.ServeHTTP(&w, &req)
|
||||||
// if w.statusCode != http.StatusOK {
|
if w.statusCode != http.StatusOK {
|
||||||
// b.Fatalf("status code is not 200: %d", w.statusCode)
|
b.Fatalf("status code is not 200: %d", w.statusCode)
|
||||||
// }
|
}
|
||||||
// if string(w.written) != "1" {
|
if string(w.written) != "1" {
|
||||||
// b.Fatalf("written is not 1: %s", string(w.written))
|
b.Fatalf("written is not 1: %s", string(w.written))
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkEntrypoint(b *testing.B) {
|
func BenchmarkEntrypoint(b *testing.B) {
|
||||||
var ep Entrypoint
|
task := task.GetTestTask(b)
|
||||||
|
ep := NewEntrypoint(task, nil)
|
||||||
req := http.Request{
|
req := http.Request{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
URL: &url.URL{Path: "/", RawPath: "/"},
|
URL: &url.URL{Path: "/", RawPath: "/"},
|
||||||
Host: "test.domain.tld",
|
Host: "test.domain.tld",
|
||||||
}
|
}
|
||||||
ep.SetFindRouteDomains([]string{})
|
ep.SetFindRouteDomains([]string{})
|
||||||
|
entrypoint.SetCtx(task, ep)
|
||||||
|
|
||||||
r := &route.Route{
|
r, err := route.NewStartedTestRoute(b, &route.Route{
|
||||||
Alias: "test",
|
Alias: "test",
|
||||||
Scheme: routeTypes.SchemeHTTP,
|
Scheme: routeTypes.SchemeHTTP,
|
||||||
Host: "localhost",
|
Host: "localhost",
|
||||||
@@ -128,29 +132,23 @@ func BenchmarkEntrypoint(b *testing.B) {
|
|||||||
HealthCheck: types.HealthCheckConfig{
|
HealthCheck: types.HealthCheckConfig{
|
||||||
Disable: true,
|
Disable: true,
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
|
||||||
err := r.Validate()
|
require.NoError(b, err)
|
||||||
if err != nil {
|
require.False(b, r.ShouldExclude())
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.Start(task.RootTask("test", false))
|
r.(types.ReverseProxyRoute).ReverseProxy().Transport = noopTransport{}
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rev, ok := routes.HTTP.Get("test")
|
|
||||||
if !ok {
|
|
||||||
b.Fatal("route not found")
|
|
||||||
}
|
|
||||||
rev.(types.ReverseProxyRoute).ReverseProxy().Transport = noopTransport{}
|
|
||||||
|
|
||||||
var w noopResponseWriter
|
var w noopResponseWriter
|
||||||
|
|
||||||
|
server, ok := ep.GetServer(common.ProxyHTTPAddr)
|
||||||
|
if !ok {
|
||||||
|
b.Fatal("server not found")
|
||||||
|
}
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for b.Loop() {
|
for b.Loop() {
|
||||||
ep.ServeHTTP(&w, &req)
|
server.ServeHTTP(&w, &req)
|
||||||
if w.statusCode != http.StatusOK {
|
if w.statusCode != http.StatusOK {
|
||||||
b.Fatalf("status code is not 200: %d", w.statusCode)
|
b.Fatalf("status code is not 200: %d", w.statusCode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,48 +3,70 @@ package entrypoint_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
. "github.com/yusing/godoxy/internal/entrypoint"
|
. "github.com/yusing/godoxy/internal/entrypoint"
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/route"
|
"github.com/yusing/godoxy/internal/route"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
routeTypes "github.com/yusing/godoxy/internal/route/types"
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
"github.com/yusing/goutils/task"
|
||||||
expect "github.com/yusing/goutils/testing"
|
expect "github.com/yusing/goutils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ep = NewEntrypoint()
|
func addRoute(t *testing.T, alias string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
func addRoute(alias string) {
|
ep := entrypoint.FromCtx(task.GetTestTask(t).Context())
|
||||||
routes.HTTP.Add(&route.ReveseProxyRoute{
|
require.NotNil(t, ep)
|
||||||
Route: &route.Route{
|
|
||||||
Alias: alias,
|
_, err := route.NewStartedTestRoute(t, &route.Route{
|
||||||
Port: route.Port{
|
Alias: alias,
|
||||||
Proxy: 80,
|
Scheme: routeTypes.SchemeHTTP,
|
||||||
},
|
Port: route.Port{
|
||||||
|
Listening: 1000,
|
||||||
|
Proxy: 8080,
|
||||||
|
},
|
||||||
|
HealthCheck: types.HealthCheckConfig{
|
||||||
|
Disable: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
route, ok := ep.HTTPRoutes().Get(alias)
|
||||||
|
require.True(t, ok, "route not found")
|
||||||
|
require.NotNil(t, route)
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(t *testing.T, match []string, noMatch []string) {
|
func run(t *testing.T, ep *Entrypoint, match []string, noMatch []string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Cleanup(routes.Clear)
|
|
||||||
t.Cleanup(func() { ep.SetFindRouteDomains(nil) })
|
server, ok := ep.GetServer(":1000")
|
||||||
|
require.True(t, ok, "server not found")
|
||||||
|
require.NotNil(t, server)
|
||||||
|
|
||||||
for _, test := range match {
|
for _, test := range match {
|
||||||
t.Run(test, func(t *testing.T) {
|
t.Run(test, func(t *testing.T) {
|
||||||
found := ep.FindRoute(test)
|
route := server.FindRoute(test)
|
||||||
expect.NotNil(t, found)
|
assert.NotNil(t, route)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range noMatch {
|
for _, test := range noMatch {
|
||||||
t.Run(test, func(t *testing.T) {
|
t.Run(test, func(t *testing.T) {
|
||||||
found := ep.FindRoute(test)
|
found, ok := ep.HTTPRoutes().Get(test)
|
||||||
expect.Nil(t, found)
|
assert.False(t, ok)
|
||||||
|
assert.Nil(t, found)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindRouteAnyDomain(t *testing.T) {
|
func TestFindRouteAnyDomain(t *testing.T) {
|
||||||
addRoute("app1")
|
ep := NewTestEntrypoint(t, nil)
|
||||||
|
|
||||||
|
addRoute(t, "app1")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1.com",
|
"app1.com",
|
||||||
@@ -58,10 +80,12 @@ func TestFindRouteAnyDomain(t *testing.T) {
|
|||||||
"app2.sub.domain.com",
|
"app2.sub.domain.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
run(t, tests, testsNoMatch)
|
run(t, ep, tests, testsNoMatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindRouteExactHostMatch(t *testing.T) {
|
func TestFindRouteExactHostMatch(t *testing.T) {
|
||||||
|
ep := NewTestEntrypoint(t, nil)
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app2.com",
|
"app2.com",
|
||||||
"app2.domain.com",
|
"app2.domain.com",
|
||||||
@@ -75,19 +99,20 @@ func TestFindRouteExactHostMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
addRoute(test)
|
addRoute(t, test)
|
||||||
}
|
}
|
||||||
|
|
||||||
run(t, tests, testsNoMatch)
|
run(t, ep, tests, testsNoMatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindRouteByDomains(t *testing.T) {
|
func TestFindRouteByDomains(t *testing.T) {
|
||||||
|
ep := NewTestEntrypoint(t, nil)
|
||||||
ep.SetFindRouteDomains([]string{
|
ep.SetFindRouteDomains([]string{
|
||||||
".domain.com",
|
".domain.com",
|
||||||
".sub.domain.com",
|
".sub.domain.com",
|
||||||
})
|
})
|
||||||
|
|
||||||
addRoute("app1")
|
addRoute(t, "app1")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1.domain.com",
|
"app1.domain.com",
|
||||||
@@ -103,16 +128,17 @@ func TestFindRouteByDomains(t *testing.T) {
|
|||||||
"app2.sub.domain.com",
|
"app2.sub.domain.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
run(t, tests, testsNoMatch)
|
run(t, ep, tests, testsNoMatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindRouteByDomainsExactMatch(t *testing.T) {
|
func TestFindRouteByDomainsExactMatch(t *testing.T) {
|
||||||
|
ep := NewTestEntrypoint(t, nil)
|
||||||
ep.SetFindRouteDomains([]string{
|
ep.SetFindRouteDomains([]string{
|
||||||
".domain.com",
|
".domain.com",
|
||||||
".sub.domain.com",
|
".sub.domain.com",
|
||||||
})
|
})
|
||||||
|
|
||||||
addRoute("app1.foo.bar")
|
addRoute(t, "app1.foo.bar")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1.foo.bar", // exact match
|
"app1.foo.bar", // exact match
|
||||||
@@ -126,13 +152,14 @@ func TestFindRouteByDomainsExactMatch(t *testing.T) {
|
|||||||
"app1.sub.domain.com",
|
"app1.sub.domain.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
run(t, tests, testsNoMatch)
|
run(t, ep, tests, testsNoMatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindRouteWithPort(t *testing.T) {
|
func TestFindRouteWithPort(t *testing.T) {
|
||||||
t.Run("AnyDomain", func(t *testing.T) {
|
t.Run("AnyDomain", func(t *testing.T) {
|
||||||
addRoute("app1")
|
ep := NewTestEntrypoint(t, nil)
|
||||||
addRoute("app2.com")
|
addRoute(t, "app1")
|
||||||
|
addRoute(t, "app2.com")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1:8080",
|
"app1:8080",
|
||||||
@@ -144,16 +171,17 @@ func TestFindRouteWithPort(t *testing.T) {
|
|||||||
"app2.co",
|
"app2.co",
|
||||||
"app2.co:8080",
|
"app2.co:8080",
|
||||||
}
|
}
|
||||||
run(t, tests, testsNoMatch)
|
run(t, ep, tests, testsNoMatch)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ByDomains", func(t *testing.T) {
|
t.Run("ByDomains", func(t *testing.T) {
|
||||||
|
ep := NewTestEntrypoint(t, nil)
|
||||||
ep.SetFindRouteDomains([]string{
|
ep.SetFindRouteDomains([]string{
|
||||||
".domain.com",
|
".domain.com",
|
||||||
})
|
})
|
||||||
addRoute("app1")
|
addRoute(t, "app1")
|
||||||
addRoute("app2")
|
addRoute(t, "app2")
|
||||||
addRoute("app3.domain.com")
|
addRoute(t, "app3.domain.com")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1.domain.com:8080",
|
"app1.domain.com:8080",
|
||||||
@@ -169,6 +197,120 @@ func TestFindRouteWithPort(t *testing.T) {
|
|||||||
"app3.domain.co",
|
"app3.domain.co",
|
||||||
"app3.domain.co:8080",
|
"app3.domain.co:8080",
|
||||||
}
|
}
|
||||||
run(t, tests, testsNoMatch)
|
run(t, ep, tests, testsNoMatch)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHealthInfoQueries(t *testing.T) {
|
||||||
|
ep := NewTestEntrypoint(t, nil)
|
||||||
|
|
||||||
|
// Add routes without health monitors (default case)
|
||||||
|
addRoute(t, "app1")
|
||||||
|
addRoute(t, "app2")
|
||||||
|
|
||||||
|
// Test GetHealthInfo
|
||||||
|
t.Run("GetHealthInfo", func(t *testing.T) {
|
||||||
|
info := ep.GetHealthInfo()
|
||||||
|
expect.Equal(t, 2, len(info))
|
||||||
|
for _, health := range info {
|
||||||
|
expect.Equal(t, types.StatusUnknown, health.Status)
|
||||||
|
expect.Equal(t, "n/a", health.Detail)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetHealthInfoWithoutDetail
|
||||||
|
t.Run("GetHealthInfoWithoutDetail", func(t *testing.T) {
|
||||||
|
info := ep.GetHealthInfoWithoutDetail()
|
||||||
|
expect.Equal(t, 2, len(info))
|
||||||
|
for _, health := range info {
|
||||||
|
expect.Equal(t, types.StatusUnknown, health.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test GetHealthInfoSimple
|
||||||
|
t.Run("GetHealthInfoSimple", func(t *testing.T) {
|
||||||
|
info := ep.GetHealthInfoSimple()
|
||||||
|
expect.Equal(t, 2, len(info))
|
||||||
|
for _, status := range info {
|
||||||
|
expect.Equal(t, types.StatusUnknown, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoutesByProvider(t *testing.T) {
|
||||||
|
ep := NewTestEntrypoint(t, nil)
|
||||||
|
|
||||||
|
// Add routes with provider info
|
||||||
|
addRoute(t, "app1")
|
||||||
|
addRoute(t, "app2")
|
||||||
|
|
||||||
|
byProvider := ep.RoutesByProvider()
|
||||||
|
expect.Equal(t, 1, len(byProvider)) // All routes are from same implicit provider
|
||||||
|
|
||||||
|
routes, ok := byProvider[""]
|
||||||
|
expect.True(t, ok)
|
||||||
|
expect.Equal(t, 2, len(routes))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumRoutes(t *testing.T) {
|
||||||
|
ep := NewTestEntrypoint(t, nil)
|
||||||
|
|
||||||
|
expect.Equal(t, 0, ep.NumRoutes())
|
||||||
|
|
||||||
|
addRoute(t, "app1")
|
||||||
|
expect.Equal(t, 1, ep.NumRoutes())
|
||||||
|
|
||||||
|
addRoute(t, "app2")
|
||||||
|
expect.Equal(t, 2, ep.NumRoutes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIterRoutes(t *testing.T) {
|
||||||
|
ep := NewTestEntrypoint(t, nil)
|
||||||
|
|
||||||
|
addRoute(t, "app1")
|
||||||
|
addRoute(t, "app2")
|
||||||
|
addRoute(t, "app3")
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for r := range ep.IterRoutes {
|
||||||
|
count++
|
||||||
|
expect.NotNil(t, r)
|
||||||
|
}
|
||||||
|
expect.Equal(t, 3, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRoute(t *testing.T) {
|
||||||
|
ep := NewTestEntrypoint(t, nil)
|
||||||
|
|
||||||
|
// Route not found case
|
||||||
|
_, ok := ep.GetRoute("nonexistent")
|
||||||
|
expect.False(t, ok)
|
||||||
|
|
||||||
|
addRoute(t, "app1")
|
||||||
|
|
||||||
|
route, ok := ep.GetRoute("app1")
|
||||||
|
expect.True(t, ok)
|
||||||
|
expect.NotNil(t, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPRoutesPool(t *testing.T) {
|
||||||
|
ep := NewTestEntrypoint(t, nil)
|
||||||
|
|
||||||
|
pool := ep.HTTPRoutes()
|
||||||
|
expect.Equal(t, 0, pool.Size())
|
||||||
|
|
||||||
|
addRoute(t, "app1")
|
||||||
|
expect.Equal(t, 1, pool.Size())
|
||||||
|
|
||||||
|
// Verify route is accessible
|
||||||
|
route, ok := pool.Get("app1")
|
||||||
|
expect.True(t, ok)
|
||||||
|
expect.NotNil(t, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExcludedRoutesPool(t *testing.T) {
|
||||||
|
ep := NewTestEntrypoint(t, nil)
|
||||||
|
|
||||||
|
excludedPool := ep.ExcludedRoutes()
|
||||||
|
expect.Equal(t, 0, excludedPool.Size())
|
||||||
|
}
|
||||||
|
|||||||
51
internal/entrypoint/http_pool_adapter.go
Normal file
51
internal/entrypoint/http_pool_adapter.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yusing/godoxy/internal/common"
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// httpPoolAdapter implements the PoolLike interface for the HTTP routes.
|
||||||
|
type httpPoolAdapter struct {
|
||||||
|
ep *Entrypoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPPoolAdapter(ep *Entrypoint) httpPoolAdapter {
|
||||||
|
return httpPoolAdapter{ep: ep}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h httpPoolAdapter) Iter(yield func(alias string, route types.HTTPRoute) bool) {
|
||||||
|
for addr, srv := range h.ep.servers.Range {
|
||||||
|
// default routes are added to both HTTP and HTTPS servers, we don't need to iterate over them twice.
|
||||||
|
if addr == common.ProxyHTTPSAddr {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for alias, route := range srv.routes.Iter {
|
||||||
|
if !yield(alias, route) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h httpPoolAdapter) Get(alias string) (types.HTTPRoute, bool) {
|
||||||
|
for addr, srv := range h.ep.servers.Range {
|
||||||
|
if addr == common.ProxyHTTPSAddr {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if route, ok := srv.routes.Get(alias); ok {
|
||||||
|
return route, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h httpPoolAdapter) Size() (n int) {
|
||||||
|
for addr, srv := range h.ep.servers.Range {
|
||||||
|
if addr == common.ProxyHTTPSAddr {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n += srv.routes.Size()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
173
internal/entrypoint/http_server.go
Normal file
173
internal/entrypoint/http_server.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
acl "github.com/yusing/godoxy/internal/acl/types"
|
||||||
|
autocert "github.com/yusing/godoxy/internal/autocert/types"
|
||||||
|
"github.com/yusing/godoxy/internal/common"
|
||||||
|
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||||
|
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||||
|
"github.com/yusing/godoxy/internal/net/gphttp/middleware/errorpage"
|
||||||
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
"github.com/yusing/goutils/pool"
|
||||||
|
"github.com/yusing/goutils/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
// httpServer is a server that listens on a given address and serves HTTP routes.
|
||||||
|
type HTTPServer interface {
|
||||||
|
Listen(addr string, proto HTTPProto) error
|
||||||
|
AddRoute(route types.HTTPRoute)
|
||||||
|
DelRoute(route types.HTTPRoute)
|
||||||
|
FindRoute(s string) types.HTTPRoute
|
||||||
|
ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpServer struct {
|
||||||
|
ep *Entrypoint
|
||||||
|
|
||||||
|
stopFunc func(reason any)
|
||||||
|
|
||||||
|
addr string
|
||||||
|
routes *pool.Pool[types.HTTPRoute]
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPProto string
|
||||||
|
|
||||||
|
const (
|
||||||
|
HTTPProtoHTTP HTTPProto = "http"
|
||||||
|
HTTPProtoHTTPS HTTPProto = "https"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewHTTPServer(ep *Entrypoint) HTTPServer {
|
||||||
|
return newHTTPServer(ep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPServer(ep *Entrypoint) *httpServer {
|
||||||
|
return &httpServer{ep: ep}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen starts the server and stop when entrypoint is stopped.
|
||||||
|
func (srv *httpServer) Listen(addr string, proto HTTPProto) error {
|
||||||
|
if srv.addr != "" {
|
||||||
|
return errors.New("server already started")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := server.Options{
|
||||||
|
Name: addr,
|
||||||
|
Handler: srv,
|
||||||
|
ACL: acl.FromCtx(srv.ep.task.Context()),
|
||||||
|
SupportProxyProtocol: srv.ep.cfg.SupportProxyProtocol,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch proto {
|
||||||
|
case HTTPProtoHTTP:
|
||||||
|
opts.HTTPAddr = addr
|
||||||
|
case HTTPProtoHTTPS:
|
||||||
|
opts.HTTPSAddr = addr
|
||||||
|
opts.CertProvider = autocert.FromCtx(srv.ep.task.Context())
|
||||||
|
}
|
||||||
|
|
||||||
|
task := srv.ep.task.Subtask("http_server", false)
|
||||||
|
_, err := server.StartServer(task, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv.stopFunc = task.FinishAndWait
|
||||||
|
srv.addr = addr
|
||||||
|
srv.routes = pool.New[types.HTTPRoute](fmt.Sprintf("[%s] %s", proto, addr))
|
||||||
|
srv.routes.DisableLog(srv.ep.httpPoolDisableLog.Load())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *httpServer) Close() {
|
||||||
|
if srv.stopFunc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
srv.stopFunc(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *httpServer) AddRoute(route types.HTTPRoute) {
|
||||||
|
srv.routes.Add(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *httpServer) DelRoute(route types.HTTPRoute) {
|
||||||
|
srv.routes.Del(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *httpServer) FindRoute(s string) types.HTTPRoute {
|
||||||
|
return srv.ep.findRouteFunc(srv.routes, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if srv.ep.accessLogger != nil {
|
||||||
|
rec := accesslog.GetResponseRecorder(w)
|
||||||
|
w = rec
|
||||||
|
defer func() {
|
||||||
|
srv.ep.accessLogger.LogRequest(r, rec.Response())
|
||||||
|
accesslog.PutResponseRecorder(rec)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
route := srv.ep.findRouteFunc(srv.routes, r.Host)
|
||||||
|
switch {
|
||||||
|
case route != nil:
|
||||||
|
r = routes.WithRouteContext(r, route)
|
||||||
|
if srv.ep.middleware != nil {
|
||||||
|
srv.ep.middleware.ServeHTTP(route.ServeHTTP, w, r)
|
||||||
|
} else {
|
||||||
|
route.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
case srv.tryHandleShortLink(w, r):
|
||||||
|
return
|
||||||
|
case srv.ep.notFoundHandler != nil:
|
||||||
|
srv.ep.notFoundHandler.ServeHTTP(w, r)
|
||||||
|
default:
|
||||||
|
serveNotFound(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *httpServer) tryHandleShortLink(w http.ResponseWriter, r *http.Request) (handled bool) {
|
||||||
|
host := r.Host
|
||||||
|
if before, _, ok := strings.Cut(host, ":"); ok {
|
||||||
|
host = before
|
||||||
|
}
|
||||||
|
if strings.EqualFold(host, common.ShortLinkPrefix) {
|
||||||
|
if srv.ep.middleware != nil {
|
||||||
|
srv.ep.middleware.ServeHTTP(srv.ep.shortLinkMatcher.ServeHTTP, w, r)
|
||||||
|
} else {
|
||||||
|
srv.ep.shortLinkMatcher.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveNotFound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Why use StatusNotFound instead of StatusBadRequest or StatusBadGateway?
|
||||||
|
// On nginx, when route for domain does not exist, it returns StatusBadGateway.
|
||||||
|
// Then scraper / scanners will know the subdomain is invalid.
|
||||||
|
// With StatusNotFound, they won't know whether it's the path, or the subdomain that is invalid.
|
||||||
|
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
|
||||||
|
log.Error().
|
||||||
|
Str("method", r.Method).
|
||||||
|
Str("url", r.URL.String()).
|
||||||
|
Str("remote", r.RemoteAddr).
|
||||||
|
Msgf("not found: %s", r.Host)
|
||||||
|
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
|
||||||
|
if ok {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
if _, err := w.Write(errorPage); err != nil {
|
||||||
|
log.Err(err).Msg("failed to write error page")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
internal/entrypoint/query.go
Normal file
91
internal/entrypoint/query.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetHealthInfo returns a map of route name to health info.
|
||||||
|
//
|
||||||
|
// The health info is for all routes, including excluded routes.
|
||||||
|
func (ep *Entrypoint) GetHealthInfo() map[string]types.HealthInfo {
|
||||||
|
healthMap := make(map[string]types.HealthInfo, ep.NumRoutes())
|
||||||
|
for r := range ep.IterRoutes {
|
||||||
|
healthMap[r.Name()] = getHealthInfo(r)
|
||||||
|
}
|
||||||
|
return healthMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHealthInfoWithoutDetail returns a map of route name to health info without detail.
|
||||||
|
//
|
||||||
|
// The health info is for all routes, including excluded routes.
|
||||||
|
func (ep *Entrypoint) GetHealthInfoWithoutDetail() map[string]types.HealthInfoWithoutDetail {
|
||||||
|
healthMap := make(map[string]types.HealthInfoWithoutDetail, ep.NumRoutes())
|
||||||
|
for r := range ep.IterRoutes {
|
||||||
|
healthMap[r.Name()] = getHealthInfoWithoutDetail(r)
|
||||||
|
}
|
||||||
|
return healthMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHealthInfoSimple returns a map of route name to health status.
|
||||||
|
//
|
||||||
|
// The health status is for all routes, including excluded routes.
|
||||||
|
func (ep *Entrypoint) GetHealthInfoSimple() map[string]types.HealthStatus {
|
||||||
|
healthMap := make(map[string]types.HealthStatus, ep.NumRoutes())
|
||||||
|
for r := range ep.IterRoutes {
|
||||||
|
healthMap[r.Name()] = getHealthInfoSimple(r)
|
||||||
|
}
|
||||||
|
return healthMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoutesByProvider returns a map of provider name to routes.
|
||||||
|
//
|
||||||
|
// The routes are all routes, including excluded routes.
|
||||||
|
func (ep *Entrypoint) RoutesByProvider() map[string][]types.Route {
|
||||||
|
rts := make(map[string][]types.Route)
|
||||||
|
for r := range ep.IterRoutes {
|
||||||
|
rts[r.ProviderName()] = append(rts[r.ProviderName()], r)
|
||||||
|
}
|
||||||
|
return rts
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHealthInfo(r types.Route) types.HealthInfo {
|
||||||
|
mon := r.HealthMonitor()
|
||||||
|
if mon == nil {
|
||||||
|
return types.HealthInfo{
|
||||||
|
HealthInfoWithoutDetail: types.HealthInfoWithoutDetail{
|
||||||
|
Status: types.StatusUnknown,
|
||||||
|
},
|
||||||
|
Detail: "n/a",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.HealthInfo{
|
||||||
|
HealthInfoWithoutDetail: types.HealthInfoWithoutDetail{
|
||||||
|
Status: mon.Status(),
|
||||||
|
Uptime: mon.Uptime(),
|
||||||
|
Latency: mon.Latency(),
|
||||||
|
},
|
||||||
|
Detail: mon.Detail(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHealthInfoWithoutDetail(r types.Route) types.HealthInfoWithoutDetail {
|
||||||
|
mon := r.HealthMonitor()
|
||||||
|
if mon == nil {
|
||||||
|
return types.HealthInfoWithoutDetail{
|
||||||
|
Status: types.StatusUnknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.HealthInfoWithoutDetail{
|
||||||
|
Status: mon.Status(),
|
||||||
|
Uptime: mon.Uptime(),
|
||||||
|
Latency: mon.Latency(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHealthInfoSimple(r types.Route) types.HealthStatus {
|
||||||
|
mon := r.HealthMonitor()
|
||||||
|
if mon == nil {
|
||||||
|
return types.StatusUnknown
|
||||||
|
}
|
||||||
|
return mon.Status()
|
||||||
|
}
|
||||||
145
internal/entrypoint/routes.go
Normal file
145
internal/entrypoint/routes.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/yusing/godoxy/internal/common"
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ep *Entrypoint) IterRoutes(yield func(r types.Route) bool) {
|
||||||
|
for _, r := range ep.HTTPRoutes().Iter {
|
||||||
|
if !yield(r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, r := range ep.streamRoutes.Iter {
|
||||||
|
if !yield(r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, r := range ep.excludedRoutes.Iter {
|
||||||
|
if !yield(r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) NumRoutes() int {
|
||||||
|
return ep.HTTPRoutes().Size() + ep.streamRoutes.Size() + ep.excludedRoutes.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) GetRoute(alias string) (types.Route, bool) {
|
||||||
|
if r, ok := ep.HTTPRoutes().Get(alias); ok {
|
||||||
|
return r, true
|
||||||
|
}
|
||||||
|
if r, ok := ep.streamRoutes.Get(alias); ok {
|
||||||
|
return r, true
|
||||||
|
}
|
||||||
|
if r, ok := ep.excludedRoutes.Get(alias); ok {
|
||||||
|
return r, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) StartAddRoute(r types.Route) error {
|
||||||
|
if r.ShouldExclude() {
|
||||||
|
ep.excludedRoutes.Add(r)
|
||||||
|
r.Task().OnCancel("remove_route", func() {
|
||||||
|
ep.excludedRoutes.Del(r)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch r := r.(type) {
|
||||||
|
case types.HTTPRoute:
|
||||||
|
if err := ep.AddHTTPRoute(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ep.shortLinkMatcher.AddRoute(r.Key())
|
||||||
|
r.Task().OnCancel("remove_route", func() {
|
||||||
|
ep.delHTTPRoute(r)
|
||||||
|
ep.shortLinkMatcher.DelRoute(r.Key())
|
||||||
|
})
|
||||||
|
case types.StreamRoute:
|
||||||
|
err := r.ListenAndServe(r.Task().Context(), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ep.streamRoutes.Add(r)
|
||||||
|
|
||||||
|
r.Task().OnCancel("remove_route", func() {
|
||||||
|
r.Stream().Close()
|
||||||
|
ep.streamRoutes.Del(r)
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown route type: %T", r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAddr(route types.HTTPRoute) (httpAddr, httpsAddr string) {
|
||||||
|
if port := route.ListenURL().Port(); port == "" || port == "0" {
|
||||||
|
host := route.ListenURL().Hostname()
|
||||||
|
if host == "" {
|
||||||
|
httpAddr = common.ProxyHTTPAddr
|
||||||
|
httpsAddr = common.ProxyHTTPSAddr
|
||||||
|
} else {
|
||||||
|
httpAddr = net.JoinHostPort(host, strconv.Itoa(common.ProxyHTTPPort))
|
||||||
|
httpsAddr = net.JoinHostPort(host, strconv.Itoa(common.ProxyHTTPSPort))
|
||||||
|
}
|
||||||
|
return httpAddr, httpsAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
httpsAddr = route.ListenURL().Host
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHTTPRoute adds a HTTP route to the entrypoint's server.
|
||||||
|
//
|
||||||
|
// If the server does not exist, it will be created, started and return any error.
|
||||||
|
func (ep *Entrypoint) AddHTTPRoute(route types.HTTPRoute) error {
|
||||||
|
httpAddr, httpsAddr := getAddr(route)
|
||||||
|
var httpErr, httpsErr error
|
||||||
|
if httpAddr != "" {
|
||||||
|
httpErr = ep.addHTTPRoute(route, httpAddr, HTTPProtoHTTP)
|
||||||
|
}
|
||||||
|
if httpsAddr != "" {
|
||||||
|
httpsErr = ep.addHTTPRoute(route, httpsAddr, HTTPProtoHTTPS)
|
||||||
|
}
|
||||||
|
return errors.Join(httpErr, httpsErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) addHTTPRoute(route types.HTTPRoute, addr string, proto HTTPProto) error {
|
||||||
|
var err error
|
||||||
|
srv, _ := ep.servers.LoadOrCompute(addr, func() (newSrv *httpServer, cancel bool) {
|
||||||
|
newSrv = newHTTPServer(ep)
|
||||||
|
err = newSrv.Listen(addr, proto)
|
||||||
|
cancel = err != nil
|
||||||
|
return
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.AddRoute(route)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) delHTTPRoute(route types.HTTPRoute) {
|
||||||
|
httpAddr, httpsAddr := getAddr(route)
|
||||||
|
if httpAddr != "" {
|
||||||
|
srv, _ := ep.servers.Load(httpAddr)
|
||||||
|
if srv != nil {
|
||||||
|
srv.DelRoute(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if httpsAddr != "" {
|
||||||
|
srv, _ := ep.servers.Load(httpsAddr)
|
||||||
|
if srv != nil {
|
||||||
|
srv.DelRoute(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,15 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
. "github.com/yusing/godoxy/internal/entrypoint"
|
. "github.com/yusing/godoxy/internal/entrypoint"
|
||||||
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestShortLinkMatcher_FQDNAlias(t *testing.T) {
|
func TestShortLinkMatcher_FQDNAlias(t *testing.T) {
|
||||||
ep := NewEntrypoint()
|
ep := NewEntrypoint(task.GetTestTask(t), nil)
|
||||||
matcher := ep.ShortLinkMatcher()
|
matcher := ep.ShortLinkMatcher()
|
||||||
matcher.AddRoute("app.domain.com")
|
matcher.AddRoute("app.domain.com")
|
||||||
|
|
||||||
@@ -45,7 +47,7 @@ func TestShortLinkMatcher_FQDNAlias(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestShortLinkMatcher_SubdomainAlias(t *testing.T) {
|
func TestShortLinkMatcher_SubdomainAlias(t *testing.T) {
|
||||||
ep := NewEntrypoint()
|
ep := NewEntrypoint(task.GetTestTask(t), nil)
|
||||||
matcher := ep.ShortLinkMatcher()
|
matcher := ep.ShortLinkMatcher()
|
||||||
matcher.SetDefaultDomainSuffix(".example.com")
|
matcher.SetDefaultDomainSuffix(".example.com")
|
||||||
matcher.AddRoute("app")
|
matcher.AddRoute("app")
|
||||||
@@ -70,7 +72,7 @@ func TestShortLinkMatcher_SubdomainAlias(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestShortLinkMatcher_NotFound(t *testing.T) {
|
func TestShortLinkMatcher_NotFound(t *testing.T) {
|
||||||
ep := NewEntrypoint()
|
ep := NewEntrypoint(task.GetTestTask(t), nil)
|
||||||
matcher := ep.ShortLinkMatcher()
|
matcher := ep.ShortLinkMatcher()
|
||||||
matcher.SetDefaultDomainSuffix(".example.com")
|
matcher.SetDefaultDomainSuffix(".example.com")
|
||||||
matcher.AddRoute("app")
|
matcher.AddRoute("app")
|
||||||
@@ -93,7 +95,7 @@ func TestShortLinkMatcher_NotFound(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestShortLinkMatcher_AddDelRoute(t *testing.T) {
|
func TestShortLinkMatcher_AddDelRoute(t *testing.T) {
|
||||||
ep := NewEntrypoint()
|
ep := NewEntrypoint(task.GetTestTask(t), nil)
|
||||||
matcher := ep.ShortLinkMatcher()
|
matcher := ep.ShortLinkMatcher()
|
||||||
matcher.SetDefaultDomainSuffix(".example.com")
|
matcher.SetDefaultDomainSuffix(".example.com")
|
||||||
|
|
||||||
@@ -131,7 +133,7 @@ func TestShortLinkMatcher_AddDelRoute(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestShortLinkMatcher_NoDefaultDomainSuffix(t *testing.T) {
|
func TestShortLinkMatcher_NoDefaultDomainSuffix(t *testing.T) {
|
||||||
ep := NewEntrypoint()
|
ep := NewEntrypoint(task.GetTestTask(t), nil)
|
||||||
matcher := ep.ShortLinkMatcher()
|
matcher := ep.ShortLinkMatcher()
|
||||||
// no SetDefaultDomainSuffix called
|
// no SetDefaultDomainSuffix called
|
||||||
|
|
||||||
@@ -158,15 +160,19 @@ func TestShortLinkMatcher_NoDefaultDomainSuffix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEntrypoint_ShortLinkDispatch(t *testing.T) {
|
func TestEntrypoint_ShortLinkDispatch(t *testing.T) {
|
||||||
ep := NewEntrypoint()
|
ep := NewEntrypoint(task.GetTestTask(t), nil)
|
||||||
ep.ShortLinkMatcher().SetDefaultDomainSuffix(".example.com")
|
ep.ShortLinkMatcher().SetDefaultDomainSuffix(".example.com")
|
||||||
ep.ShortLinkMatcher().AddRoute("app")
|
ep.ShortLinkMatcher().AddRoute("app")
|
||||||
|
|
||||||
|
server := NewHTTPServer(ep)
|
||||||
|
err := server.Listen("localhost:0", HTTPProtoHTTP)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("shortlink host", func(t *testing.T) {
|
t.Run("shortlink host", func(t *testing.T) {
|
||||||
req := httptest.NewRequest("GET", "/app", nil)
|
req := httptest.NewRequest("GET", "/app", nil)
|
||||||
req.Host = common.ShortLinkPrefix
|
req.Host = common.ShortLinkPrefix
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
ep.ServeHTTP(w, req)
|
server.ServeHTTP(w, req)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
|
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
|
||||||
assert.Equal(t, "https://app.example.com/", w.Header().Get("Location"))
|
assert.Equal(t, "https://app.example.com/", w.Header().Get("Location"))
|
||||||
@@ -176,7 +182,7 @@ func TestEntrypoint_ShortLinkDispatch(t *testing.T) {
|
|||||||
req := httptest.NewRequest("GET", "/app", nil)
|
req := httptest.NewRequest("GET", "/app", nil)
|
||||||
req.Host = common.ShortLinkPrefix + ":8080"
|
req.Host = common.ShortLinkPrefix + ":8080"
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
ep.ServeHTTP(w, req)
|
server.ServeHTTP(w, req)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
|
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
|
||||||
assert.Equal(t, "https://app.example.com/", w.Header().Get("Location"))
|
assert.Equal(t, "https://app.example.com/", w.Header().Get("Location"))
|
||||||
@@ -186,7 +192,7 @@ func TestEntrypoint_ShortLinkDispatch(t *testing.T) {
|
|||||||
req := httptest.NewRequest("GET", "/app", nil)
|
req := httptest.NewRequest("GET", "/app", nil)
|
||||||
req.Host = "app.example.com"
|
req.Host = "app.example.com"
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
ep.ServeHTTP(w, req)
|
server.ServeHTTP(w, req)
|
||||||
|
|
||||||
// Should not redirect, should try normal route lookup (which will 404)
|
// Should not redirect, should try normal route lookup (which will 404)
|
||||||
assert.NotEqual(t, http.StatusTemporaryRedirect, w.Code)
|
assert.NotEqual(t, http.StatusTemporaryRedirect, w.Code)
|
||||||
|
|||||||
18
internal/entrypoint/types/context.go
Normal file
18
internal/entrypoint/types/context.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContextKey struct{}
|
||||||
|
|
||||||
|
func SetCtx(ctx interface{ SetValue(any, any) }, ep Entrypoint) {
|
||||||
|
ctx.SetValue(ContextKey{}, ep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromCtx(ctx context.Context) Entrypoint {
|
||||||
|
if ep, ok := ctx.Value(ContextKey{}).(Entrypoint); ok {
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
37
internal/entrypoint/types/entrypoint.go
Normal file
37
internal/entrypoint/types/entrypoint.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entrypoint interface {
|
||||||
|
SupportProxyProtocol() bool
|
||||||
|
|
||||||
|
DisablePoolsLog(v bool)
|
||||||
|
|
||||||
|
GetRoute(alias string) (types.Route, bool)
|
||||||
|
StartAddRoute(r types.Route) error
|
||||||
|
IterRoutes(yield func(r types.Route) bool)
|
||||||
|
NumRoutes() int
|
||||||
|
RoutesByProvider() map[string][]types.Route
|
||||||
|
|
||||||
|
HTTPRoutes() PoolLike[types.HTTPRoute]
|
||||||
|
StreamRoutes() PoolLike[types.StreamRoute]
|
||||||
|
ExcludedRoutes() RWPoolLike[types.Route]
|
||||||
|
|
||||||
|
GetHealthInfo() map[string]types.HealthInfo
|
||||||
|
GetHealthInfoWithoutDetail() map[string]types.HealthInfoWithoutDetail
|
||||||
|
GetHealthInfoSimple() map[string]types.HealthStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
type PoolLike[Route types.Route] interface {
|
||||||
|
Get(alias string) (Route, bool)
|
||||||
|
Iter(yield func(alias string, r Route) bool)
|
||||||
|
Size() int
|
||||||
|
}
|
||||||
|
|
||||||
|
type RWPoolLike[Route types.Route] interface {
|
||||||
|
PoolLike[Route]
|
||||||
|
Add(r Route)
|
||||||
|
Del(r Route)
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
var _ nettypes.Stream = (*Watcher)(nil)
|
var _ nettypes.Stream = (*Watcher)(nil)
|
||||||
|
|
||||||
// ListenAndServe implements nettypes.Stream.
|
// ListenAndServe implements nettypes.Stream.
|
||||||
func (w *Watcher) ListenAndServe(ctx context.Context, predial, onRead nettypes.HookFunc) {
|
func (w *Watcher) ListenAndServe(ctx context.Context, predial, onRead nettypes.HookFunc) error {
|
||||||
w.stream.ListenAndServe(ctx, func(ctx context.Context) error { //nolint:contextcheck
|
return w.stream.ListenAndServe(ctx, func(ctx context.Context) error { //nolint:contextcheck
|
||||||
return w.preDial(ctx, predial)
|
return w.preDial(ctx, predial)
|
||||||
}, func(ctx context.Context) error {
|
}, func(ctx context.Context) error {
|
||||||
return w.onRead(ctx, onRead)
|
return w.onRead(ctx, onRead)
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import (
|
|||||||
"github.com/yusing/ds/ordered"
|
"github.com/yusing/ds/ordered"
|
||||||
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"
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/health/monitor"
|
"github.com/yusing/godoxy/internal/health/monitor"
|
||||||
"github.com/yusing/godoxy/internal/idlewatcher/provider"
|
"github.com/yusing/godoxy/internal/idlewatcher/provider"
|
||||||
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
"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"
|
||||||
@@ -173,7 +173,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
depRoute, ok = routes.GetIncludeExcluded(dep)
|
depRoute, ok = entrypoint.FromCtx(parent.Context()).GetRoute(dep)
|
||||||
if !ok {
|
if !ok {
|
||||||
depErrors.Addf("dependency %q not found", dep)
|
depErrors.Addf("dependency %q not found", dep)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ func (p *Poller[T, AggregateT]) pollWithTimeout(ctx context.Context) {
|
|||||||
p.lastResult.Store(data)
|
p.lastResult.Store(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Poller[T, AggregateT]) Start() {
|
func (p *Poller[T, AggregateT]) Start(parent task.Parent) {
|
||||||
t := task.RootTask("poller."+p.name, true)
|
t := parent.Subtask("poller."+p.name, true)
|
||||||
l := log.With().Str("name", p.name).Logger()
|
l := log.With().Str("name", p.name).Logger()
|
||||||
err := p.load()
|
err := p.load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,22 +2,24 @@ package uptime
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/metrics/period"
|
"github.com/yusing/godoxy/internal/metrics/period"
|
||||||
metricsutils "github.com/yusing/godoxy/internal/metrics/utils"
|
metricsutils "github.com/yusing/godoxy/internal/metrics/utils"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
StatusByAlias struct {
|
StatusByAlias struct {
|
||||||
Map map[string]routes.HealthInfoWithoutDetail `json:"statuses"`
|
Map map[string]types.HealthInfoWithoutDetail `json:"statuses"`
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
} // @name RouteStatusesByAlias
|
} // @name RouteStatusesByAlias
|
||||||
Status struct {
|
Status struct {
|
||||||
Status types.HealthStatus `json:"status" swaggertype:"string" enums:"healthy,unhealthy,unknown,napping,starting"`
|
Status types.HealthStatus `json:"status" swaggertype:"string" enums:"healthy,unhealthy,unknown,napping,starting"`
|
||||||
@@ -40,8 +42,12 @@ type (
|
|||||||
var Poller = period.NewPoller("uptime", getStatuses, aggregateStatuses)
|
var Poller = period.NewPoller("uptime", getStatuses, aggregateStatuses)
|
||||||
|
|
||||||
func getStatuses(ctx context.Context, _ StatusByAlias) (StatusByAlias, error) {
|
func getStatuses(ctx context.Context, _ StatusByAlias) (StatusByAlias, error) {
|
||||||
|
ep := entrypoint.FromCtx(ctx)
|
||||||
|
if ep == nil {
|
||||||
|
return StatusByAlias{}, errors.New("entrypoint not found in context")
|
||||||
|
}
|
||||||
return StatusByAlias{
|
return StatusByAlias{
|
||||||
Map: routes.GetHealthInfoWithoutDetail(),
|
Map: ep.GetHealthInfoWithoutDetail(),
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -127,11 +133,14 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
|
|||||||
up, down, idle, latency := rs.calculateInfo(statuses)
|
up, down, idle, latency := rs.calculateInfo(statuses)
|
||||||
|
|
||||||
status := types.StatusUnknown
|
status := types.StatusUnknown
|
||||||
r, ok := routes.GetIncludeExcluded(alias)
|
if state := config.ActiveState.Load(); state != nil {
|
||||||
if ok {
|
// FIXME: pass ctx to getRoute
|
||||||
mon := r.HealthMonitor()
|
r, ok := entrypoint.FromCtx(state.Context()).GetRoute(alias)
|
||||||
if mon != nil {
|
if ok {
|
||||||
status = mon.Status()
|
mon := r.HealthMonitor()
|
||||||
|
if mon != nil {
|
||||||
|
status = mon.Status()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/yusing/godoxy/internal/common"
|
||||||
"github.com/yusing/godoxy/internal/entrypoint"
|
"github.com/yusing/godoxy/internal/entrypoint"
|
||||||
. "github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
. "github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||||
"github.com/yusing/godoxy/internal/route"
|
"github.com/yusing/godoxy/internal/route"
|
||||||
routeTypes "github.com/yusing/godoxy/internal/route/types"
|
routeTypes "github.com/yusing/godoxy/internal/route/types"
|
||||||
"github.com/yusing/goutils/http/reverseproxy"
|
"github.com/yusing/goutils/http/reverseproxy"
|
||||||
"github.com/yusing/goutils/task"
|
|
||||||
expect "github.com/yusing/goutils/testing"
|
expect "github.com/yusing/goutils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -230,15 +230,15 @@ func TestEntrypointBypassRoute(t *testing.T) {
|
|||||||
portInt, err := strconv.Atoi(port)
|
portInt, err := strconv.Atoi(port)
|
||||||
expect.NoError(t, err)
|
expect.NoError(t, err)
|
||||||
|
|
||||||
expect.NoError(t, err)
|
entry := entrypoint.NewTestEntrypoint(t, nil)
|
||||||
entry := entrypoint.NewEntrypoint()
|
_, err = route.NewStartedTestRoute(t, &route.Route{
|
||||||
r := &route.Route{
|
|
||||||
Alias: "test-route",
|
Alias: "test-route",
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: routeTypes.Port{
|
Port: routeTypes.Port{
|
||||||
Proxy: portInt,
|
Proxy: portInt,
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
expect.NoError(t, err)
|
||||||
|
|
||||||
err = entry.SetMiddlewares([]map[string]any{
|
err = entry.SetMiddlewares([]map[string]any{
|
||||||
{
|
{
|
||||||
@@ -254,13 +254,13 @@ func TestEntrypointBypassRoute(t *testing.T) {
|
|||||||
})
|
})
|
||||||
expect.NoError(t, err)
|
expect.NoError(t, err)
|
||||||
|
|
||||||
err = r.Validate()
|
|
||||||
expect.NoError(t, err)
|
|
||||||
r.Start(task.RootTask("test", false))
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest("GET", "http://test-route.example.com", nil)
|
req := httptest.NewRequest("GET", "http://test-route.example.com", nil)
|
||||||
entry.ServeHTTP(recorder, req)
|
server, ok := entry.GetServer(common.ProxyHTTPAddr)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("server not found")
|
||||||
|
}
|
||||||
|
server.ServeHTTP(recorder, req)
|
||||||
expect.Equal(t, recorder.Code, http.StatusOK, "should bypass http redirect")
|
expect.Equal(t, recorder.Code, http.StatusOK, "should bypass http redirect")
|
||||||
expect.Equal(t, recorder.Body.String(), "test")
|
expect.Equal(t, recorder.Body.String(), "test")
|
||||||
expect.Equal(t, recorder.Header().Get("Test-Header"), "test-value")
|
expect.Equal(t, recorder.Header().Get("Test-Header"), "test-value")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
httputils "github.com/yusing/goutils/http"
|
httputils "github.com/yusing/goutils/http"
|
||||||
ioutils "github.com/yusing/goutils/io"
|
ioutils "github.com/yusing/goutils/io"
|
||||||
)
|
)
|
||||||
@@ -66,7 +66,7 @@ func (m *crowdsecMiddleware) finalize() error {
|
|||||||
// before implements RequestModifier.
|
// before implements RequestModifier.
|
||||||
func (m *crowdsecMiddleware) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
func (m *crowdsecMiddleware) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||||
// Build CrowdSec URL
|
// Build CrowdSec URL
|
||||||
crowdsecURL, err := m.buildCrowdSecURL()
|
crowdsecURL, err := m.buildCrowdSecURL(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Crowdsec.LogError(r).Err(err).Msg("failed to build CrowdSec URL")
|
Crowdsec.LogError(r).Err(err).Msg("failed to build CrowdSec URL")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
@@ -167,10 +167,10 @@ func (m *crowdsecMiddleware) before(w http.ResponseWriter, r *http.Request) (pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildCrowdSecURL constructs the CrowdSec server URL based on route or IP configuration
|
// buildCrowdSecURL constructs the CrowdSec server URL based on route or IP configuration
|
||||||
func (m *crowdsecMiddleware) buildCrowdSecURL() (string, error) {
|
func (m *crowdsecMiddleware) buildCrowdSecURL(ctx context.Context) (string, error) {
|
||||||
// Try to get route first
|
// Try to get route first
|
||||||
if m.Route != "" {
|
if m.Route != "" {
|
||||||
if route, ok := routes.HTTP.Get(m.Route); ok {
|
if route, ok := entrypoint.FromCtx(ctx).GetRoute(m.Route); ok {
|
||||||
// Using route name
|
// Using route name
|
||||||
targetURL := *route.TargetURL()
|
targetURL := *route.TargetURL()
|
||||||
targetURL.Path = m.Endpoint
|
targetURL.Path = m.Endpoint
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
httputils "github.com/yusing/goutils/http"
|
httputils "github.com/yusing/goutils/http"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
)
|
)
|
||||||
@@ -46,7 +46,7 @@ func (m *forwardAuthMiddleware) setup() {
|
|||||||
|
|
||||||
// before implements RequestModifier.
|
// before implements RequestModifier.
|
||||||
func (m *forwardAuthMiddleware) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
func (m *forwardAuthMiddleware) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||||
route, ok := routes.HTTP.Get(m.Route)
|
route, ok := entrypoint.FromCtx(r.Context()).HTTPRoutes().Get(m.Route)
|
||||||
if !ok {
|
if !ok {
|
||||||
ForwardAuth.LogWarn(r).Str("route", m.Route).Msg("forwardauth route not found")
|
ForwardAuth.LogWarn(r).Str("route", m.Route).Msg("forwardauth route not found")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Stream interface {
|
type Stream interface {
|
||||||
ListenAndServe(ctx context.Context, preDial, onRead HookFunc)
|
ListenAndServe(ctx context.Context, preDial, onRead HookFunc) error
|
||||||
LocalAddr() net.Addr
|
LocalAddr() net.Addr
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ Internal package with stable core types. Route configuration schema is versioned
|
|||||||
type Route struct {
|
type Route struct {
|
||||||
Alias string // Unique route identifier
|
Alias string // Unique route identifier
|
||||||
Scheme Scheme // http, https, h2c, tcp, udp, fileserver
|
Scheme Scheme // http, https, h2c, tcp, udp, fileserver
|
||||||
Host string // Virtual host
|
Host string // Target host
|
||||||
Port Port // Listen and target ports
|
Port Port // Listen and target ports
|
||||||
|
|
||||||
|
Bind string // Bind address for listening (IP address, optional)
|
||||||
|
|
||||||
// File serving
|
// File serving
|
||||||
Root string // Document root
|
Root string // Document root
|
||||||
SPA bool // Single-page app mode
|
SPA bool // Single-page app mode
|
||||||
@@ -196,6 +198,7 @@ type Route struct {
|
|||||||
Alias string `json:"alias"`
|
Alias string `json:"alias"`
|
||||||
Scheme Scheme `json:"scheme"`
|
Scheme Scheme `json:"scheme"`
|
||||||
Host string `json:"host,omitempty"`
|
Host string `json:"host,omitempty"`
|
||||||
|
Bind string `json:"bind,omitempty"` // Listen bind address
|
||||||
Port Port `json:"port"`
|
Port Port `json:"port"`
|
||||||
Root string `json:"root,omitempty"`
|
Root string `json:"root,omitempty"`
|
||||||
SPA bool `json:"spa,omitempty"`
|
SPA bool `json:"spa,omitempty"`
|
||||||
@@ -218,23 +221,26 @@ labels:
|
|||||||
routes:
|
routes:
|
||||||
myapp:
|
myapp:
|
||||||
scheme: http
|
scheme: http
|
||||||
root: /var/www/myapp
|
host: myapp.local
|
||||||
spa: true
|
bind: 192.168.1.100 # Optional: bind to specific address
|
||||||
|
port:
|
||||||
|
proxy: 80
|
||||||
|
target: 3000
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependency and Integration Map
|
## Dependency and Integration Map
|
||||||
|
|
||||||
| Dependency | Purpose |
|
| Dependency | Purpose |
|
||||||
| -------------------------------- | -------------------------------- |
|
| ---------------------------------- | --------------------------------- |
|
||||||
| `internal/route/routes` | Route registry and lookup |
|
| `internal/route/routes/context.go` | Route context helpers (only file) |
|
||||||
| `internal/route/rules` | Request/response rule processing |
|
| `internal/route/rules` | Request/response rule processing |
|
||||||
| `internal/route/stream` | TCP/UDP stream proxying |
|
| `internal/route/stream` | TCP/UDP stream proxying |
|
||||||
| `internal/route/provider` | Route discovery and loading |
|
| `internal/route/provider` | Route discovery and loading |
|
||||||
| `internal/health/monitor` | Health checking |
|
| `internal/health/monitor` | Health checking |
|
||||||
| `internal/idlewatcher` | Idle container management |
|
| `internal/idlewatcher` | Idle container management |
|
||||||
| `internal/logging/accesslog` | Request logging |
|
| `internal/logging/accesslog` | Request logging |
|
||||||
| `internal/homepage` | Dashboard integration |
|
| `internal/homepage` | Dashboard integration |
|
||||||
| `github.com/yusing/goutils/errs` | Error handling |
|
| `github.com/yusing/goutils/errs` | Error handling |
|
||||||
|
|
||||||
## Observability
|
## Observability
|
||||||
|
|
||||||
@@ -305,6 +311,18 @@ route := &route.Route{
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Route with Custom Bind Address
|
||||||
|
|
||||||
|
```go
|
||||||
|
route := &route.Route{
|
||||||
|
Alias: "myapp",
|
||||||
|
Scheme: route.SchemeHTTP,
|
||||||
|
Host: "myapp.local",
|
||||||
|
Bind: "192.168.1.100", // Bind to specific interface
|
||||||
|
Port: route.Port{Listening: 8443, Proxy: 80},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### File Server Route
|
### File Server Route
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
package route
|
package route
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"context"
|
||||||
|
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkExists(r types.Route) gperr.Error {
|
// checkExists checks if the route already exists in the entrypoint.
|
||||||
|
//
|
||||||
|
// Context must be passed from the parent task that carries the entrypoint value.
|
||||||
|
func checkExists(ctx context.Context, r types.Route) gperr.Error {
|
||||||
if r.UseLoadBalance() { // skip checking for load balanced routes
|
if r.UseLoadBalance() { // skip checking for load balanced routes
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -16,9 +21,9 @@ func checkExists(r types.Route) gperr.Error {
|
|||||||
)
|
)
|
||||||
switch r := r.(type) {
|
switch r := r.(type) {
|
||||||
case types.HTTPRoute:
|
case types.HTTPRoute:
|
||||||
existing, ok = routes.HTTP.Get(r.Key())
|
existing, ok = entrypoint.FromCtx(ctx).HTTPRoutes().Get(r.Key())
|
||||||
case types.StreamRoute:
|
case types.StreamRoute:
|
||||||
existing, ok = routes.Stream.Get(r.Key())
|
existing, ok = entrypoint.FromCtx(ctx).StreamRoutes().Get(r.Key())
|
||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
return gperr.Errorf("route already exists: from provider %s and %s", existing.ProviderName(), r.ProviderName())
|
return gperr.Errorf("route already exists: from provider %s and %s", existing.ProviderName(), r.ProviderName())
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
"github.com/rs/zerolog/log"
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/health/monitor"
|
"github.com/yusing/godoxy/internal/health/monitor"
|
||||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
@@ -120,20 +120,23 @@ func (s *FileServer) Start(parent task.Parent) gperr.Error {
|
|||||||
if s.UseHealthCheck() {
|
if s.UseHealthCheck() {
|
||||||
s.HealthMon = monitor.NewMonitor(s)
|
s.HealthMon = monitor.NewMonitor(s)
|
||||||
if err := s.HealthMon.Start(s.task); err != nil {
|
if err := s.HealthMon.Start(s.task); err != nil {
|
||||||
return err
|
l := log.With().Str("type", "fileserver").Str("name", s.Name()).Logger()
|
||||||
|
gperr.LogWarn("health monitor error", err, &l)
|
||||||
|
s.HealthMon = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
routes.HTTP.Add(s)
|
ep := entrypoint.FromCtx(parent.Context())
|
||||||
if state := config.WorkingState.Load(); state != nil {
|
if ep == nil {
|
||||||
state.ShortLinkMatcher().AddRoute(s.Alias)
|
err := gperr.New("entrypoint not initialized")
|
||||||
|
s.task.Finish(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ep.StartAddRoute(s); err != nil {
|
||||||
|
s.task.Finish(err)
|
||||||
|
return gperr.Wrap(err)
|
||||||
}
|
}
|
||||||
s.task.OnFinished("remove_route_from_http", func() {
|
|
||||||
routes.HTTP.Del(s)
|
|
||||||
if state := config.WorkingState.Load(); state != nil {
|
|
||||||
state.ShortLinkMatcher().DelRoute(s.Alias)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agentproxy"
|
"github.com/yusing/godoxy/agent/pkg/agentproxy"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/health/monitor"
|
"github.com/yusing/godoxy/internal/health/monitor"
|
||||||
"github.com/yusing/godoxy/internal/idlewatcher"
|
"github.com/yusing/godoxy/internal/idlewatcher"
|
||||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/yusing/godoxy/internal/net/gphttp/loadbalancer"
|
"github.com/yusing/godoxy/internal/net/gphttp/loadbalancer"
|
||||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
route "github.com/yusing/godoxy/internal/route/types"
|
route "github.com/yusing/godoxy/internal/route/types"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
@@ -159,23 +158,28 @@ func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error {
|
|||||||
|
|
||||||
if r.HealthMon != nil {
|
if r.HealthMon != nil {
|
||||||
if err := r.HealthMon.Start(r.task); err != nil {
|
if err := r.HealthMon.Start(r.task); err != nil {
|
||||||
return err
|
gperr.LogWarn("health monitor error", err, &r.rp.Logger)
|
||||||
|
r.HealthMon = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ep := entrypoint.FromCtx(parent.Context())
|
||||||
|
if ep == nil {
|
||||||
|
err := gperr.New("entrypoint not initialized")
|
||||||
|
r.task.Finish(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if r.UseLoadBalance() {
|
if r.UseLoadBalance() {
|
||||||
r.addToLoadBalancer(parent)
|
if err := r.addToLoadBalancer(parent, ep); err != nil {
|
||||||
} else {
|
r.task.Finish(err)
|
||||||
routes.HTTP.Add(r)
|
return gperr.Wrap(err)
|
||||||
if state := config.WorkingState.Load(); state != nil {
|
}
|
||||||
state.ShortLinkMatcher().AddRoute(r.Alias)
|
} else {
|
||||||
|
if err := ep.StartAddRoute(r); err != nil {
|
||||||
|
r.task.Finish(err)
|
||||||
|
return gperr.Wrap(err)
|
||||||
}
|
}
|
||||||
r.task.OnCancel("remove_route", func() {
|
|
||||||
routes.HTTP.Del(r)
|
|
||||||
if state := config.WorkingState.Load(); state != nil {
|
|
||||||
state.ShortLinkMatcher().DelRoute(r.Alias)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -187,16 +191,16 @@ func (r *ReveseProxyRoute) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
var lbLock sync.Mutex
|
var lbLock sync.Mutex
|
||||||
|
|
||||||
func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
|
func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent, ep entrypoint.Entrypoint) error {
|
||||||
var lb *loadbalancer.LoadBalancer
|
var lb *loadbalancer.LoadBalancer
|
||||||
cfg := r.LoadBalance
|
cfg := r.LoadBalance
|
||||||
lbLock.Lock()
|
lbLock.Lock()
|
||||||
|
defer lbLock.Unlock()
|
||||||
|
|
||||||
l, ok := routes.HTTP.Get(cfg.Link)
|
l, ok := ep.HTTPRoutes().Get(cfg.Link)
|
||||||
var linked *ReveseProxyRoute
|
var linked *ReveseProxyRoute
|
||||||
if ok {
|
if ok {
|
||||||
lbLock.Unlock()
|
linked = l.(*ReveseProxyRoute) // it must be a reverse proxy route
|
||||||
linked = l.(*ReveseProxyRoute)
|
|
||||||
lb = linked.loadBalancer
|
lb = linked.loadBalancer
|
||||||
lb.UpdateConfigIfNeeded(cfg)
|
lb.UpdateConfigIfNeeded(cfg)
|
||||||
if linked.Homepage.Name == "" {
|
if linked.Homepage.Name == "" {
|
||||||
@@ -209,22 +213,20 @@ func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
|
|||||||
Route: &Route{
|
Route: &Route{
|
||||||
Alias: cfg.Link,
|
Alias: cfg.Link,
|
||||||
Homepage: r.Homepage,
|
Homepage: r.Homepage,
|
||||||
|
Bind: r.Bind,
|
||||||
|
Metadata: Metadata{
|
||||||
|
LisURL: r.ListenURL(),
|
||||||
|
task: lb.Task(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
loadBalancer: lb,
|
loadBalancer: lb,
|
||||||
handler: lb,
|
handler: lb,
|
||||||
}
|
}
|
||||||
linked.SetHealthMonitor(lb)
|
linked.SetHealthMonitor(lb)
|
||||||
routes.HTTP.AddKey(cfg.Link, linked)
|
if err := ep.StartAddRoute(linked); err != nil {
|
||||||
if state := config.WorkingState.Load(); state != nil {
|
lb.Finish(err)
|
||||||
state.ShortLinkMatcher().AddRoute(cfg.Link)
|
return err
|
||||||
}
|
}
|
||||||
r.task.OnFinished("remove_loadbalancer_route", func() {
|
|
||||||
routes.HTTP.DelKey(cfg.Link)
|
|
||||||
if state := config.WorkingState.Load(); state != nil {
|
|
||||||
state.ShortLinkMatcher().DelRoute(cfg.Link)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
lbLock.Unlock()
|
|
||||||
}
|
}
|
||||||
r.loadBalancer = lb
|
r.loadBalancer = lb
|
||||||
|
|
||||||
@@ -233,4 +235,5 @@ func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
|
|||||||
r.task.OnCancel("lb_remove_server", func() {
|
r.task.OnCancel("lb_remove_server", func() {
|
||||||
lb.RemoveServer(server)
|
lb.RemoveServer(server)
|
||||||
})
|
})
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
39
internal/route/reverse_proxy_test.go
Normal file
39
internal/route/reverse_proxy_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
route "github.com/yusing/godoxy/internal/route/types"
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReverseProxyRoute(t *testing.T) {
|
||||||
|
t.Run("LinkToLoadBalancer", func(t *testing.T) {
|
||||||
|
cfg := Route{
|
||||||
|
Alias: "test",
|
||||||
|
Scheme: route.SchemeHTTP,
|
||||||
|
Host: "example.com",
|
||||||
|
Port: Port{Proxy: 80},
|
||||||
|
LoadBalance: &types.LoadBalancerConfig{
|
||||||
|
Link: "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cfg1 := Route{
|
||||||
|
Alias: "test1",
|
||||||
|
Scheme: route.SchemeHTTP,
|
||||||
|
Host: "example.com",
|
||||||
|
Port: Port{Proxy: 80},
|
||||||
|
LoadBalance: &types.LoadBalancerConfig{
|
||||||
|
Link: "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r, err := NewStartedTestRoute(t, &cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, r)
|
||||||
|
r2, err := NewStartedTestRoute(t, &cfg1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, r2)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/yusing/godoxy/internal/agentpool"
|
"github.com/yusing/godoxy/internal/agentpool"
|
||||||
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"
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/health/monitor"
|
"github.com/yusing/godoxy/internal/health/monitor"
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||||
@@ -33,7 +34,6 @@ import (
|
|||||||
|
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
"github.com/yusing/godoxy/internal/route/rules"
|
"github.com/yusing/godoxy/internal/route/rules"
|
||||||
rulepresets "github.com/yusing/godoxy/internal/route/rules/presets"
|
rulepresets "github.com/yusing/godoxy/internal/route/rules/presets"
|
||||||
route "github.com/yusing/godoxy/internal/route/types"
|
route "github.com/yusing/godoxy/internal/route/types"
|
||||||
@@ -46,7 +46,6 @@ type (
|
|||||||
Host string `json:"host,omitempty"`
|
Host string `json:"host,omitempty"`
|
||||||
Port route.Port `json:"port"`
|
Port route.Port `json:"port"`
|
||||||
|
|
||||||
// for TCP and UDP routes, bind address to listen on
|
|
||||||
Bind string `json:"bind,omitempty" validate:"omitempty,ip_addr" extensions:"x-nullable"`
|
Bind string `json:"bind,omitempty" validate:"omitempty,ip_addr" extensions:"x-nullable"`
|
||||||
|
|
||||||
Root string `json:"root,omitempty"`
|
Root string `json:"root,omitempty"`
|
||||||
@@ -200,7 +199,11 @@ func (r *Route) validate() gperr.Error {
|
|||||||
|
|
||||||
if (r.Proxmox == nil || r.Proxmox.Node == "" || r.Proxmox.VMID == nil) && r.Container == nil {
|
if (r.Proxmox == nil || r.Proxmox.Node == "" || r.Proxmox.VMID == nil) && r.Container == nil {
|
||||||
wasNotNil := r.Proxmox != nil
|
wasNotNil := r.Proxmox != nil
|
||||||
proxmoxProviders := config.WorkingState.Load().Value().Providers.Proxmox
|
workingState := config.WorkingState.Load()
|
||||||
|
var proxmoxProviders []*proxmox.Config
|
||||||
|
if workingState != nil { // nil in tests
|
||||||
|
proxmoxProviders = workingState.Value().Providers.Proxmox
|
||||||
|
}
|
||||||
if len(proxmoxProviders) > 0 {
|
if len(proxmoxProviders) > 0 {
|
||||||
// it's fine if ip is nil
|
// it's fine if ip is nil
|
||||||
hostname := r.Host
|
hostname := r.Host
|
||||||
@@ -274,24 +277,19 @@ func (r *Route) validate() gperr.Error {
|
|||||||
var impl types.Route
|
var impl types.Route
|
||||||
var err gperr.Error
|
var err gperr.Error
|
||||||
|
|
||||||
switch r.Scheme {
|
if r.ShouldExclude() {
|
||||||
case route.SchemeFileServer:
|
|
||||||
r.Host = ""
|
|
||||||
r.Port.Proxy = 0
|
|
||||||
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, "file://"+r.Root)
|
|
||||||
case route.SchemeHTTP, route.SchemeHTTPS, route.SchemeH2C:
|
|
||||||
if r.Port.Listening != 0 {
|
|
||||||
errs.Addf("unexpected listening port for %s scheme", r.Scheme)
|
|
||||||
}
|
|
||||||
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s", r.Scheme, net.JoinHostPort(r.Host, strconv.Itoa(r.Port.Proxy))))
|
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s", r.Scheme, net.JoinHostPort(r.Host, strconv.Itoa(r.Port.Proxy))))
|
||||||
case route.SchemeTCP, route.SchemeUDP:
|
} else {
|
||||||
if r.ShouldExclude() {
|
switch r.Scheme {
|
||||||
// should exclude, we don't care the scheme here.
|
case route.SchemeFileServer:
|
||||||
|
r.Host = ""
|
||||||
|
r.Port.Proxy = 0
|
||||||
|
r.LisURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("https://%s", net.JoinHostPort(r.Bind, strconv.Itoa(r.Port.Listening))))
|
||||||
|
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, "file://"+r.Root)
|
||||||
|
case route.SchemeHTTP, route.SchemeHTTPS, route.SchemeH2C:
|
||||||
|
r.LisURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("https://%s", net.JoinHostPort(r.Bind, strconv.Itoa(r.Port.Listening))))
|
||||||
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s", r.Scheme, net.JoinHostPort(r.Host, strconv.Itoa(r.Port.Proxy))))
|
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s", r.Scheme, net.JoinHostPort(r.Host, strconv.Itoa(r.Port.Proxy))))
|
||||||
} else {
|
case route.SchemeTCP, route.SchemeUDP:
|
||||||
if r.Bind == "" {
|
|
||||||
r.Bind = "0.0.0.0"
|
|
||||||
}
|
|
||||||
bindIP := net.ParseIP(r.Bind)
|
bindIP := net.ParseIP(r.Bind)
|
||||||
remoteIP := net.ParseIP(r.Host)
|
remoteIP := net.ParseIP(r.Host)
|
||||||
toNetwork := func(ip net.IP, scheme route.Scheme) string {
|
toNetwork := func(ip net.IP, scheme route.Scheme) string {
|
||||||
@@ -360,8 +358,8 @@ func (r *Route) validateRules() error {
|
|||||||
return errors.New("rule preset `webui.yml` not found")
|
return errors.New("rule preset `webui.yml` not found")
|
||||||
}
|
}
|
||||||
r.Rules = rules
|
r.Rules = rules
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.RuleFile != "" && len(r.Rules) > 0 {
|
if r.RuleFile != "" && len(r.Rules) > 0 {
|
||||||
@@ -504,7 +502,7 @@ func (r *Route) start(parent task.Parent) gperr.Error {
|
|||||||
// skip checking for excluded routes
|
// skip checking for excluded routes
|
||||||
excluded := r.ShouldExclude()
|
excluded := r.ShouldExclude()
|
||||||
if !excluded {
|
if !excluded {
|
||||||
if err := checkExists(r); err != nil {
|
if err := checkExists(parent.Context(), r); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -518,15 +516,22 @@ func (r *Route) start(parent task.Parent) gperr.Error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
r.task = parent.Subtask("excluded."+r.Name(), true)
|
ep := entrypoint.FromCtx(parent.Context())
|
||||||
routes.Excluded.Add(r.impl)
|
if ep == nil {
|
||||||
|
return gperr.New("entrypoint not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.task = parent.Subtask("excluded."+r.Name(), false)
|
||||||
|
ep.ExcludedRoutes().Add(r.impl)
|
||||||
r.task.OnCancel("remove_route_from_excluded", func() {
|
r.task.OnCancel("remove_route_from_excluded", func() {
|
||||||
routes.Excluded.Del(r.impl)
|
ep.ExcludedRoutes().Del(r.impl)
|
||||||
})
|
})
|
||||||
if r.UseHealthCheck() {
|
if r.UseHealthCheck() {
|
||||||
r.HealthMon = monitor.NewMonitor(r.impl)
|
r.HealthMon = monitor.NewMonitor(r.impl)
|
||||||
err := r.HealthMon.Start(r.task)
|
err := r.HealthMon.Start(r.task)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -564,6 +569,10 @@ func (r *Route) ProviderName() string {
|
|||||||
return r.Provider
|
return r.Provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Route) ListenURL() *nettypes.URL {
|
||||||
|
return r.LisURL
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Route) TargetURL() *nettypes.URL {
|
func (r *Route) TargetURL() *nettypes.URL {
|
||||||
return r.ProxyURL
|
return r.ProxyURL
|
||||||
}
|
}
|
||||||
@@ -932,6 +941,13 @@ func (r *Route) Finalize() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch r.Scheme {
|
||||||
|
case route.SchemeTCP, route.SchemeUDP:
|
||||||
|
if r.Bind == "" {
|
||||||
|
r.Bind = "0.0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
r.Port.Listening, r.Port.Proxy = lp, pp
|
r.Port.Listening, r.Port.Proxy = lp, pp
|
||||||
|
|
||||||
workingState := config.WorkingState.Load()
|
workingState := config.WorkingState.Load()
|
||||||
@@ -942,7 +958,8 @@ func (r *Route) Finalize() {
|
|||||||
panic("bug: working state is nil")
|
panic("bug: working state is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
r.HealthCheck.ApplyDefaults(config.WorkingState.Load().Value().Defaults.HealthCheck)
|
// TODO: default value from context
|
||||||
|
r.HealthCheck.ApplyDefaults(workingState.Value().Defaults.HealthCheck)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Route) FinalizeHomepageConfig() {
|
func (r *Route) FinalizeHomepageConfig() {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
route "github.com/yusing/godoxy/internal/route/types"
|
route "github.com/yusing/godoxy/internal/route/types"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
expect "github.com/yusing/goutils/testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRouteValidate(t *testing.T) {
|
func TestRouteValidate(t *testing.T) {
|
||||||
@@ -19,20 +19,8 @@ func TestRouteValidate(t *testing.T) {
|
|||||||
Port: route.Port{Proxy: common.ProxyHTTPPort},
|
Port: route.Port{Proxy: common.ProxyHTTPPort},
|
||||||
}
|
}
|
||||||
err := r.Validate()
|
err := r.Validate()
|
||||||
expect.HasError(t, err, "Validate should return error for localhost with reserved port")
|
require.Error(t, err, "Validate should return error for localhost with reserved port")
|
||||||
expect.ErrorContains(t, err, "reserved for godoxy")
|
require.ErrorContains(t, err, "reserved for godoxy")
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ListeningPortWithHTTP", func(t *testing.T) {
|
|
||||||
r := &Route{
|
|
||||||
Alias: "test",
|
|
||||||
Scheme: route.SchemeHTTP,
|
|
||||||
Host: "example.com",
|
|
||||||
Port: route.Port{Proxy: 80, Listening: 1234},
|
|
||||||
}
|
|
||||||
err := r.Validate()
|
|
||||||
expect.HasError(t, err, "Validate should return error for HTTP scheme with listening port")
|
|
||||||
expect.ErrorContains(t, err, "unexpected listening port")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("DisabledHealthCheckWithLoadBalancer", func(t *testing.T) {
|
t.Run("DisabledHealthCheckWithLoadBalancer", func(t *testing.T) {
|
||||||
@@ -49,8 +37,8 @@ func TestRouteValidate(t *testing.T) {
|
|||||||
}, // Minimal LoadBalance config with non-empty Link will be checked by UseLoadBalance
|
}, // Minimal LoadBalance config with non-empty Link will be checked by UseLoadBalance
|
||||||
}
|
}
|
||||||
err := r.Validate()
|
err := r.Validate()
|
||||||
expect.HasError(t, err, "Validate should return error for disabled healthcheck with loadbalancer")
|
require.Error(t, err, "Validate should return error for disabled healthcheck with loadbalancer")
|
||||||
expect.ErrorContains(t, err, "cannot disable healthcheck")
|
require.ErrorContains(t, err, "cannot disable healthcheck")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("FileServerScheme", func(t *testing.T) {
|
t.Run("FileServerScheme", func(t *testing.T) {
|
||||||
@@ -62,8 +50,8 @@ func TestRouteValidate(t *testing.T) {
|
|||||||
Root: "/tmp", // Root is required for file server
|
Root: "/tmp", // Root is required for file server
|
||||||
}
|
}
|
||||||
err := r.Validate()
|
err := r.Validate()
|
||||||
expect.NoError(t, err, "Validate should not return error for valid file server route")
|
require.NoError(t, err, "Validate should not return error for valid file server route")
|
||||||
expect.NotNil(t, r.impl, "Impl should be initialized")
|
require.NotNil(t, r.impl, "Impl should be initialized")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("HTTPScheme", func(t *testing.T) {
|
t.Run("HTTPScheme", func(t *testing.T) {
|
||||||
@@ -74,8 +62,8 @@ func TestRouteValidate(t *testing.T) {
|
|||||||
Port: route.Port{Proxy: 80},
|
Port: route.Port{Proxy: 80},
|
||||||
}
|
}
|
||||||
err := r.Validate()
|
err := r.Validate()
|
||||||
expect.NoError(t, err, "Validate should not return error for valid HTTP route")
|
require.NoError(t, err, "Validate should not return error for valid HTTP route")
|
||||||
expect.NotNil(t, r.impl, "Impl should be initialized")
|
require.NotNil(t, r.impl, "Impl should be initialized")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("TCPScheme", func(t *testing.T) {
|
t.Run("TCPScheme", func(t *testing.T) {
|
||||||
@@ -86,8 +74,8 @@ func TestRouteValidate(t *testing.T) {
|
|||||||
Port: route.Port{Proxy: 80, Listening: 8080},
|
Port: route.Port{Proxy: 80, Listening: 8080},
|
||||||
}
|
}
|
||||||
err := r.Validate()
|
err := r.Validate()
|
||||||
expect.NoError(t, err, "Validate should not return error for valid TCP route")
|
require.NoError(t, err, "Validate should not return error for valid TCP route")
|
||||||
expect.NotNil(t, r.impl, "Impl should be initialized")
|
require.NotNil(t, r.impl, "Impl should be initialized")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("DockerContainer", func(t *testing.T) {
|
t.Run("DockerContainer", func(t *testing.T) {
|
||||||
@@ -106,8 +94,8 @@ func TestRouteValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := r.Validate()
|
err := r.Validate()
|
||||||
expect.NoError(t, err, "Validate should not return error for valid docker container route")
|
require.NoError(t, err, "Validate should not return error for valid docker container route")
|
||||||
expect.NotNil(t, r.ProxyURL, "ProxyURL should be set")
|
require.NotNil(t, r.ProxyURL, "ProxyURL should be set")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("InvalidScheme", func(t *testing.T) {
|
t.Run("InvalidScheme", func(t *testing.T) {
|
||||||
@@ -117,7 +105,7 @@ func TestRouteValidate(t *testing.T) {
|
|||||||
Host: "example.com",
|
Host: "example.com",
|
||||||
Port: route.Port{Proxy: 80},
|
Port: route.Port{Proxy: 80},
|
||||||
}
|
}
|
||||||
expect.Panics(t, func() {
|
require.Panics(t, func() {
|
||||||
_ = r.Validate()
|
_ = r.Validate()
|
||||||
}, "Validate should panic for invalid scheme")
|
}, "Validate should panic for invalid scheme")
|
||||||
})
|
})
|
||||||
@@ -130,9 +118,9 @@ func TestRouteValidate(t *testing.T) {
|
|||||||
Port: route.Port{Proxy: 80},
|
Port: route.Port{Proxy: 80},
|
||||||
}
|
}
|
||||||
err := r.Validate()
|
err := r.Validate()
|
||||||
expect.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expect.NotNil(t, r.ProxyURL)
|
require.NotNil(t, r.ProxyURL)
|
||||||
expect.NotNil(t, r.HealthCheck)
|
require.NotNil(t, r.HealthCheck)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +132,7 @@ func TestPreferredPort(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
port := preferredPort(ports)
|
port := preferredPort(ports)
|
||||||
expect.Equal(t, port, 3000)
|
require.Equal(t, 3000, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDockerRouteDisallowAgent(t *testing.T) {
|
func TestDockerRouteDisallowAgent(t *testing.T) {
|
||||||
@@ -164,8 +152,8 @@ func TestDockerRouteDisallowAgent(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := r.Validate()
|
err := r.Validate()
|
||||||
expect.HasError(t, err, "Validate should return error for docker route with agent")
|
require.Error(t, err, "Validate should return error for docker route with agent")
|
||||||
expect.ErrorContains(t, err, "specifying agent is not allowed for docker container routes")
|
require.ErrorContains(t, err, "specifying agent is not allowed for docker container routes")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouteAgent(t *testing.T) {
|
func TestRouteAgent(t *testing.T) {
|
||||||
@@ -177,8 +165,8 @@ func TestRouteAgent(t *testing.T) {
|
|||||||
Agent: "test-agent",
|
Agent: "test-agent",
|
||||||
}
|
}
|
||||||
err := r.Validate()
|
err := r.Validate()
|
||||||
expect.NoError(t, err, "Validate should not return error for valid route with agent")
|
require.NoError(t, err, "Validate should not return error for valid route with agent")
|
||||||
expect.NotNil(t, r.GetAgent(), "GetAgent should return agent")
|
require.NotNil(t, r.GetAgent(), "GetAgent should return agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouteApplyingHealthCheckDefaults(t *testing.T) {
|
func TestRouteApplyingHealthCheckDefaults(t *testing.T) {
|
||||||
@@ -188,6 +176,106 @@ func TestRouteApplyingHealthCheckDefaults(t *testing.T) {
|
|||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect.Equal(t, hc.Interval, 15*time.Second)
|
require.Equal(t, 15*time.Second, hc.Interval)
|
||||||
expect.Equal(t, hc.Timeout, 10*time.Second)
|
require.Equal(t, 10*time.Second, hc.Timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouteBindField(t *testing.T) {
|
||||||
|
t.Run("TCPSchemeWithCustomBind", func(t *testing.T) {
|
||||||
|
r := &Route{
|
||||||
|
Alias: "test-tcp",
|
||||||
|
Scheme: route.SchemeTCP,
|
||||||
|
Host: "192.168.1.100",
|
||||||
|
Port: route.Port{Proxy: 80, Listening: 8080},
|
||||||
|
Bind: "192.168.1.1",
|
||||||
|
}
|
||||||
|
err := r.Validate()
|
||||||
|
require.NoError(t, err, "Validate should not return error for TCP route with custom bind")
|
||||||
|
require.NotNil(t, r.LisURL, "LisURL should be set")
|
||||||
|
require.Equal(t, "tcp4://192.168.1.1:8080", r.LisURL.String(), "LisURL should contain custom bind address")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UDPSchemeWithCustomBind", func(t *testing.T) {
|
||||||
|
r := &Route{
|
||||||
|
Alias: "test-udp",
|
||||||
|
Scheme: route.SchemeUDP,
|
||||||
|
Host: "10.0.0.1",
|
||||||
|
Port: route.Port{Proxy: 53, Listening: 53},
|
||||||
|
Bind: "10.0.0.254",
|
||||||
|
}
|
||||||
|
err := r.Validate()
|
||||||
|
require.NoError(t, err, "Validate should not return error for UDP route with custom bind")
|
||||||
|
require.NotNil(t, r.LisURL, "LisURL should be set")
|
||||||
|
require.Equal(t, "udp4://10.0.0.254:53", r.LisURL.String(), "LisURL should contain custom bind address")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HTTPSchemeWithoutBind", func(t *testing.T) {
|
||||||
|
r := &Route{
|
||||||
|
Alias: "test-http",
|
||||||
|
Scheme: route.SchemeHTTP,
|
||||||
|
Host: "example.com",
|
||||||
|
Port: route.Port{Proxy: 80},
|
||||||
|
}
|
||||||
|
err := r.Validate()
|
||||||
|
require.NoError(t, err, "Validate should not return error for HTTP route without bind")
|
||||||
|
require.NotNil(t, r.LisURL, "LisURL should be set")
|
||||||
|
require.Equal(t, "https://:0", r.LisURL.String(), "LisURL should contain bind address")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HTTPSchemeWithBind", func(t *testing.T) {
|
||||||
|
r := &Route{
|
||||||
|
Alias: "test-http",
|
||||||
|
Scheme: route.SchemeHTTP,
|
||||||
|
Host: "example.com",
|
||||||
|
Port: route.Port{Proxy: 80},
|
||||||
|
Bind: "0.0.0.0",
|
||||||
|
}
|
||||||
|
err := r.Validate()
|
||||||
|
require.NoError(t, err, "Validate should not return error for HTTP route with bind")
|
||||||
|
require.NotNil(t, r.LisURL, "LisURL should be set")
|
||||||
|
require.Equal(t, "https://0.0.0.0:0", r.LisURL.String(), "LisURL should contain bind address")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HTTPSchemeWithBindAndPort", func(t *testing.T) {
|
||||||
|
r := &Route{
|
||||||
|
Alias: "test-http",
|
||||||
|
Scheme: route.SchemeHTTP,
|
||||||
|
Host: "example.com",
|
||||||
|
Port: route.Port{Listening: 8080, Proxy: 80},
|
||||||
|
Bind: "0.0.0.0",
|
||||||
|
}
|
||||||
|
err := r.Validate()
|
||||||
|
require.NoError(t, err, "Validate should not return error for HTTP route with bind and port")
|
||||||
|
require.NotNil(t, r.LisURL, "LisURL should be set")
|
||||||
|
require.Equal(t, "https://0.0.0.0:8080", r.LisURL.String(), "LisURL should contain bind address and listening port")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TCPSchemeDefaultsToZeroBind", func(t *testing.T) {
|
||||||
|
r := &Route{
|
||||||
|
Alias: "test-default-bind",
|
||||||
|
Scheme: route.SchemeTCP,
|
||||||
|
Host: "example.com",
|
||||||
|
Port: route.Port{Proxy: 80, Listening: 8080},
|
||||||
|
Bind: "",
|
||||||
|
}
|
||||||
|
err := r.Validate()
|
||||||
|
require.NoError(t, err, "Validate should not return error for TCP route with empty bind")
|
||||||
|
require.Equal(t, "0.0.0.0", r.Bind, "Bind should default to 0.0.0.0 for TCP scheme")
|
||||||
|
require.NotNil(t, r.LisURL, "LisURL should be set")
|
||||||
|
require.Equal(t, "tcp4://0.0.0.0:8080", r.LisURL.String(), "LisURL should use default bind address")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FileServerSchemeWithBind", func(t *testing.T) {
|
||||||
|
r := &Route{
|
||||||
|
Alias: "test-fileserver",
|
||||||
|
Scheme: route.SchemeFileServer,
|
||||||
|
Port: route.Port{Listening: 9000},
|
||||||
|
Root: "/tmp",
|
||||||
|
Bind: "127.0.0.1",
|
||||||
|
}
|
||||||
|
err := r.Validate()
|
||||||
|
require.NoError(t, err, "Validate should not return error for fileserver route with bind")
|
||||||
|
require.NotNil(t, r.LisURL, "LisURL should be set")
|
||||||
|
require.Equal(t, "https://127.0.0.1:9000", r.LisURL.String(), "LisURL should contain bind address")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,307 +0,0 @@
|
|||||||
# Route Registry
|
|
||||||
|
|
||||||
Provides centralized route registry with O(1) lookups and route context management for HTTP handlers.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The `internal/route/routes` package maintains the global route registry for GoDoxy. It provides thread-safe route lookups by alias, route iteration, and utilities for propagating route context through HTTP request handlers.
|
|
||||||
|
|
||||||
### Primary Consumers
|
|
||||||
|
|
||||||
- **HTTP handlers**: Lookup routes and extract request context
|
|
||||||
- **Route providers**: Register and unregister routes
|
|
||||||
- **Health system**: Query route health status
|
|
||||||
- **WebUI**: Display route information
|
|
||||||
|
|
||||||
### Non-goals
|
|
||||||
|
|
||||||
- Does not create or modify routes
|
|
||||||
- Does not handle route validation
|
|
||||||
- Does not implement routing logic (matching)
|
|
||||||
|
|
||||||
### Stability
|
|
||||||
|
|
||||||
Internal package with stable public API.
|
|
||||||
|
|
||||||
## Public API
|
|
||||||
|
|
||||||
### Route Pools
|
|
||||||
|
|
||||||
```go
|
|
||||||
var (
|
|
||||||
HTTP = pool.New[types.HTTPRoute]("http_routes")
|
|
||||||
Stream = pool.New[types.StreamRoute]("stream_routes")
|
|
||||||
Excluded = pool.New[types.Route]("excluded_routes")
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Pool methods:
|
|
||||||
|
|
||||||
- `Get(alias string) (T, bool)` - O(1) lookup
|
|
||||||
- `Add(r T)` - Register route
|
|
||||||
- `Del(r T)` - Unregister route
|
|
||||||
- `Size() int` - Route count
|
|
||||||
- `Clear()` - Remove all routes
|
|
||||||
- `Iter` - Channel-based iteration
|
|
||||||
|
|
||||||
### Exported Functions
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Iterate over active routes (HTTP + Stream)
|
|
||||||
func IterActive(yield func(r types.Route) bool)
|
|
||||||
|
|
||||||
// Iterate over all routes (HTTP + Stream + Excluded)
|
|
||||||
func IterAll(yield func(r types.Route) bool)
|
|
||||||
|
|
||||||
// Get route count
|
|
||||||
func NumActiveRoutes() int
|
|
||||||
func NumAllRoutes() int
|
|
||||||
|
|
||||||
// Clear all routes
|
|
||||||
func Clear()
|
|
||||||
|
|
||||||
// Lookup functions
|
|
||||||
func Get(alias string) (types.Route, bool)
|
|
||||||
func GetHTTPRouteOrExact(alias, host string) (types.HTTPRoute, bool)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Route Context
|
|
||||||
|
|
||||||
```go
|
|
||||||
type RouteContext struct {
|
|
||||||
context.Context
|
|
||||||
Route types.HTTPRoute
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach route to request context (uses unsafe pointer for performance)
|
|
||||||
func WithRouteContext(r *http.Request, route types.HTTPRoute) *http.Request
|
|
||||||
|
|
||||||
// Extract route from request context
|
|
||||||
func TryGetRoute(r *http.Request) types.HTTPRoute
|
|
||||||
```
|
|
||||||
|
|
||||||
### Upstream Information
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TryGetUpstreamName(r *http.Request) string
|
|
||||||
func TryGetUpstreamScheme(r *http.Request) string
|
|
||||||
func TryGetUpstreamHost(r *http.Request) string
|
|
||||||
func TryGetUpstreamPort(r *http.Request) string
|
|
||||||
func TryGetUpstreamHostPort(r *http.Request) string
|
|
||||||
func TryGetUpstreamAddr(r *http.Request) string
|
|
||||||
func TryGetUpstreamURL(r *http.Request) string
|
|
||||||
```
|
|
||||||
|
|
||||||
### Health Information
|
|
||||||
|
|
||||||
```go
|
|
||||||
type HealthInfo struct {
|
|
||||||
HealthInfoWithoutDetail
|
|
||||||
Detail string
|
|
||||||
}
|
|
||||||
|
|
||||||
type HealthInfoWithoutDetail struct {
|
|
||||||
Status types.HealthStatus
|
|
||||||
Uptime time.Duration
|
|
||||||
Latency time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetHealthInfo() map[string]HealthInfo
|
|
||||||
func GetHealthInfoWithoutDetail() map[string]HealthInfoWithoutDetail
|
|
||||||
func GetHealthInfoSimple() map[string]types.HealthStatus
|
|
||||||
```
|
|
||||||
|
|
||||||
### Provider Grouping
|
|
||||||
|
|
||||||
```go
|
|
||||||
func ByProvider() map[string][]types.Route
|
|
||||||
```
|
|
||||||
|
|
||||||
## Proxmox Integration
|
|
||||||
|
|
||||||
Routes can be automatically linked to Proxmox nodes or LXC containers through reverse lookup during validation.
|
|
||||||
|
|
||||||
### Node-Level Routes
|
|
||||||
|
|
||||||
Routes can be linked to a Proxmox node directly (VMID = 0) when the route's hostname, IP, or alias matches a node name or IP:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Route linked to Proxmox node (no specific VM)
|
|
||||||
route.Proxmox = &proxmox.NodeConfig{
|
|
||||||
Node: "pve-node-01",
|
|
||||||
VMID: 0, // node-level, no container
|
|
||||||
VMName: "",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Container-Level Routes
|
|
||||||
|
|
||||||
Routes are linked to LXC containers when they match a VM resource by hostname, IP, or alias:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Route linked to LXC container
|
|
||||||
route.Proxmox = &proxmox.NodeConfig{
|
|
||||||
Node: "pve-node-01",
|
|
||||||
VMID: 100,
|
|
||||||
VMName: "my-container",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lookup Priority
|
|
||||||
|
|
||||||
1. **Node match** - If hostname, IP, or alias matches a Proxmox node
|
|
||||||
2. **VM match** - If hostname, IP, or alias matches a VM resource
|
|
||||||
|
|
||||||
Node-level routes skip container control logic (start/check IPs) and can be used to proxy node services directly.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
classDiagram
|
|
||||||
class HTTP
|
|
||||||
class Stream
|
|
||||||
class Excluded
|
|
||||||
class RouteContext
|
|
||||||
|
|
||||||
HTTP : +Get(alias) T
|
|
||||||
HTTP : +Add(r)
|
|
||||||
HTTP : +Del(r)
|
|
||||||
HTTP : +Size() int
|
|
||||||
HTTP : +Iter chan
|
|
||||||
|
|
||||||
Stream : +Get(alias) T
|
|
||||||
Stream : +Add(r)
|
|
||||||
Stream : +Del(r)
|
|
||||||
|
|
||||||
Excluded : +Get(alias) T
|
|
||||||
Excluded : +Add(r)
|
|
||||||
Excluded : +Del(r)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Route Lookup Flow
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A[Lookup Request] --> B{HTTP Pool}
|
|
||||||
B -->|Found| C[Return Route]
|
|
||||||
B -->|Not Found| D{Stream Pool}
|
|
||||||
D -->|Found| C
|
|
||||||
D -->|Not Found| E[Return nil]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Context Propagation
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant H as HTTP Handler
|
|
||||||
participant R as Registry
|
|
||||||
participant C as RouteContext
|
|
||||||
|
|
||||||
H->>R: WithRouteContext(req, route)
|
|
||||||
R->>C: Attach route via unsafe pointer
|
|
||||||
C-->>H: Modified request
|
|
||||||
|
|
||||||
H->>R: TryGetRoute(req)
|
|
||||||
R->>C: Extract route from context
|
|
||||||
C-->>R: Route
|
|
||||||
R-->>H: Route
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dependency and Integration Map
|
|
||||||
|
|
||||||
| Dependency | Purpose |
|
|
||||||
| -------------------------------- | ---------------------------------- |
|
|
||||||
| `internal/types` | Route and health type definitions |
|
|
||||||
| `internal/proxmox` | Proxmox node/container integration |
|
|
||||||
| `github.com/yusing/goutils/pool` | Thread-safe pool implementation |
|
|
||||||
|
|
||||||
## Observability
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
|
|
||||||
Registry operations logged at DEBUG level:
|
|
||||||
|
|
||||||
- Route add/remove
|
|
||||||
- Pool iteration
|
|
||||||
- Context operations
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
|
|
||||||
- `WithRouteContext` uses `unsafe.Pointer` to avoid request cloning
|
|
||||||
- Route lookups are O(1) using internal maps
|
|
||||||
- Iteration uses channels for memory efficiency
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
- Route context propagation is internal to the process
|
|
||||||
- No sensitive data exposed in context keys
|
|
||||||
- Routes are validated before registration
|
|
||||||
|
|
||||||
## Failure Modes and Recovery
|
|
||||||
|
|
||||||
| Failure | Behavior | Recovery |
|
|
||||||
| ---------------------------------------- | ------------------------------ | -------------------- |
|
|
||||||
| Route not found | Returns (nil, false) | Verify route alias |
|
|
||||||
| Context extraction on non-route request | Returns nil | Check request origin |
|
|
||||||
| Concurrent modification during iteration | Handled by pool implementation | N/A |
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Basic Route Lookup
|
|
||||||
|
|
||||||
```go
|
|
||||||
route, ok := routes.Get("myapp")
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("route not found")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Iterating Over All Routes
|
|
||||||
|
|
||||||
```go
|
|
||||||
for r := range routes.IterActive {
|
|
||||||
log.Printf("Route: %s", r.Name())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Getting Health Status
|
|
||||||
|
|
||||||
```go
|
|
||||||
healthMap := routes.GetHealthInfo()
|
|
||||||
for name, health := range healthMap {
|
|
||||||
log.Printf("Route %s: %s (uptime: %v)", name, health.Status, health.Uptime)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Route Context in Handler
|
|
||||||
|
|
||||||
```go
|
|
||||||
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
route := routes.TryGetRoute(r)
|
|
||||||
if route == nil {
|
|
||||||
http.Error(w, "Route not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
upstreamHost := routes.TryGetUpstreamHost(r)
|
|
||||||
log.Printf("Proxying to: %s", upstreamHost)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Grouping Routes by Provider
|
|
||||||
|
|
||||||
```go
|
|
||||||
byProvider := routes.ByProvider()
|
|
||||||
for providerName, routeList := range byProvider {
|
|
||||||
log.Printf("Provider %s: %d routes", providerName, len(routeList))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Notes
|
|
||||||
|
|
||||||
- Unit tests for pool thread safety
|
|
||||||
- Context propagation tests
|
|
||||||
- Health info aggregation tests
|
|
||||||
- Provider grouping tests
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/yusing/godoxy/internal/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HealthInfo struct {
|
|
||||||
HealthInfoWithoutDetail
|
|
||||||
Detail string `json:"detail"`
|
|
||||||
} // @name HealthInfo
|
|
||||||
|
|
||||||
type HealthInfoWithoutDetail struct {
|
|
||||||
Status types.HealthStatus `json:"status" swaggertype:"string" enums:"healthy,unhealthy,napping,starting,error,unknown"`
|
|
||||||
Uptime time.Duration `json:"uptime" swaggertype:"number"` // uptime in milliseconds
|
|
||||||
Latency time.Duration `json:"latency" swaggertype:"number"` // latency in microseconds
|
|
||||||
} // @name HealthInfoWithoutDetail
|
|
||||||
|
|
||||||
type HealthMap = map[string]types.HealthStatusString // @name HealthMap
|
|
||||||
|
|
||||||
// GetHealthInfo returns a map of route name to health info.
|
|
||||||
//
|
|
||||||
// The health info is for all routes, including excluded routes.
|
|
||||||
func GetHealthInfo() map[string]HealthInfo {
|
|
||||||
healthMap := make(map[string]HealthInfo, NumAllRoutes())
|
|
||||||
for r := range IterAll {
|
|
||||||
healthMap[r.Name()] = getHealthInfo(r)
|
|
||||||
}
|
|
||||||
return healthMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHealthInfoWithoutDetail returns a map of route name to health info without detail.
|
|
||||||
//
|
|
||||||
// The health info is for all routes, including excluded routes.
|
|
||||||
func GetHealthInfoWithoutDetail() map[string]HealthInfoWithoutDetail {
|
|
||||||
healthMap := make(map[string]HealthInfoWithoutDetail, NumAllRoutes())
|
|
||||||
for r := range IterAll {
|
|
||||||
healthMap[r.Name()] = getHealthInfoWithoutDetail(r)
|
|
||||||
}
|
|
||||||
return healthMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetHealthInfoSimple() map[string]types.HealthStatus {
|
|
||||||
healthMap := make(map[string]types.HealthStatus, NumAllRoutes())
|
|
||||||
for r := range IterAll {
|
|
||||||
healthMap[r.Name()] = getHealthInfoSimple(r)
|
|
||||||
}
|
|
||||||
return healthMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func getHealthInfo(r types.Route) HealthInfo {
|
|
||||||
mon := r.HealthMonitor()
|
|
||||||
if mon == nil {
|
|
||||||
return HealthInfo{
|
|
||||||
HealthInfoWithoutDetail: HealthInfoWithoutDetail{
|
|
||||||
Status: types.StatusUnknown,
|
|
||||||
},
|
|
||||||
Detail: "n/a",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return HealthInfo{
|
|
||||||
HealthInfoWithoutDetail: HealthInfoWithoutDetail{
|
|
||||||
Status: mon.Status(),
|
|
||||||
Uptime: mon.Uptime(),
|
|
||||||
Latency: mon.Latency(),
|
|
||||||
},
|
|
||||||
Detail: mon.Detail(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getHealthInfoWithoutDetail(r types.Route) HealthInfoWithoutDetail {
|
|
||||||
mon := r.HealthMonitor()
|
|
||||||
if mon == nil {
|
|
||||||
return HealthInfoWithoutDetail{
|
|
||||||
Status: types.StatusUnknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return HealthInfoWithoutDetail{
|
|
||||||
Status: mon.Status(),
|
|
||||||
Uptime: mon.Uptime(),
|
|
||||||
Latency: mon.Latency(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getHealthInfoSimple(r types.Route) types.HealthStatus {
|
|
||||||
mon := r.HealthMonitor()
|
|
||||||
if mon == nil {
|
|
||||||
return types.StatusUnknown
|
|
||||||
}
|
|
||||||
return mon.Status()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByProvider returns a map of provider name to routes.
|
|
||||||
//
|
|
||||||
// The routes are all routes, including excluded routes.
|
|
||||||
func ByProvider() map[string][]types.Route {
|
|
||||||
rts := make(map[string][]types.Route)
|
|
||||||
for r := range IterAll {
|
|
||||||
rts[r.ProviderName()] = append(rts[r.ProviderName()], r)
|
|
||||||
}
|
|
||||||
return rts
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/yusing/godoxy/internal/types"
|
|
||||||
"github.com/yusing/goutils/pool"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
HTTP = pool.New[types.HTTPRoute]("http_routes")
|
|
||||||
Stream = pool.New[types.StreamRoute]("stream_routes")
|
|
||||||
|
|
||||||
Excluded = pool.New[types.Route]("excluded_routes")
|
|
||||||
)
|
|
||||||
|
|
||||||
func IterActive(yield func(r types.Route) bool) {
|
|
||||||
for _, r := range HTTP.Iter {
|
|
||||||
if !yield(r) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, r := range Stream.Iter {
|
|
||||||
if !yield(r) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IterAll(yield func(r types.Route) bool) {
|
|
||||||
for _, r := range HTTP.Iter {
|
|
||||||
if !yield(r) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, r := range Stream.Iter {
|
|
||||||
if !yield(r) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, r := range Excluded.Iter {
|
|
||||||
if !yield(r) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NumActiveRoutes() int {
|
|
||||||
return HTTP.Size() + Stream.Size()
|
|
||||||
}
|
|
||||||
|
|
||||||
func NumAllRoutes() int {
|
|
||||||
return HTTP.Size() + Stream.Size() + Excluded.Size()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Clear() {
|
|
||||||
HTTP.Clear()
|
|
||||||
Stream.Clear()
|
|
||||||
Excluded.Clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetHTTPRouteOrExact(alias, host string) (types.HTTPRoute, bool) {
|
|
||||||
r, ok := HTTP.Get(alias)
|
|
||||||
if ok {
|
|
||||||
return r, true
|
|
||||||
}
|
|
||||||
// try find with exact match
|
|
||||||
return HTTP.Get(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the route with the given alias.
|
|
||||||
//
|
|
||||||
// It does not return excluded routes.
|
|
||||||
func Get(alias string) (types.Route, bool) {
|
|
||||||
if r, ok := HTTP.Get(alias); ok {
|
|
||||||
return r, true
|
|
||||||
}
|
|
||||||
if r, ok := Stream.Get(alias); ok {
|
|
||||||
return r, true
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIncludeExcluded returns the route with the given alias, including excluded routes.
|
|
||||||
func GetIncludeExcluded(alias string) (types.Route, bool) {
|
|
||||||
if r, ok := HTTP.Get(alias); ok {
|
|
||||||
return r, true
|
|
||||||
}
|
|
||||||
if r, ok := Stream.Get(alias); ok {
|
|
||||||
return r, true
|
|
||||||
}
|
|
||||||
return Excluded.Get(alias)
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/logging"
|
"github.com/yusing/godoxy/internal/logging"
|
||||||
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"
|
||||||
@@ -197,9 +198,10 @@ var commands = map[string]struct {
|
|||||||
build: func(args any) CommandHandler {
|
build: func(args any) CommandHandler {
|
||||||
route := args.(string)
|
route := args.(string)
|
||||||
return TerminatingCommand(func(w http.ResponseWriter, req *http.Request) error {
|
return TerminatingCommand(func(w http.ResponseWriter, req *http.Request) error {
|
||||||
r, ok := routes.HTTP.Get(route)
|
ep := entrypoint.FromCtx(req.Context())
|
||||||
|
r, ok := ep.HTTPRoutes().Get(route)
|
||||||
if !ok {
|
if !ok {
|
||||||
excluded, has := routes.Excluded.Get(route)
|
excluded, has := ep.ExcludedRoutes().Get(route)
|
||||||
if has {
|
if has {
|
||||||
r, ok = excluded.(types.HTTPRoute)
|
r, ok = excluded.(types.HTTPRoute)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,16 @@
|
|||||||
do: pass
|
do: pass
|
||||||
- name: protected
|
- name: protected
|
||||||
on: |
|
on: |
|
||||||
!path glob("@tanstack-start/*")
|
!path glob("/@tanstack-start/*")
|
||||||
|
!path glob("/@vite-plugin-pwa/*")
|
||||||
|
!path glob("/__tsd/*")
|
||||||
!path /@react-refresh
|
!path /@react-refresh
|
||||||
!path /@vite/client
|
!path /@vite/client
|
||||||
!path regex("\?token=\w{5}-\w{5}")
|
!path regex("/\?token=[a-zA-Z0-9-_]+")
|
||||||
!path glob("/@id/*")
|
!path glob("/@id/*")
|
||||||
!path glob("/api/v1/auth/*")
|
!path glob("/api/v1/auth/*")
|
||||||
!path glob("/auth/*")
|
!path glob("/auth/*")
|
||||||
!path regex("[A-Za-z0-9_\-/]+\.(css|ts|js|mjs|svg|png|jpg|jpeg|gif|ico|webp|woff2?|eot|ttf|otf|txt)(\?.+)?")
|
!path regex("([A-Za-z0-9_\-/]+)+\.(css|ts|js|mjs|svg|png|jpg|jpeg|gif|ico|webp|woff2?|eot|ttf|otf|txt)(\?.*)?")
|
||||||
!path /api/v1/version
|
!path /api/v1/version
|
||||||
!path /manifest.webmanifest
|
!path /manifest.webmanifest
|
||||||
do: require_auth
|
do: require_auth
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
"github.com/yusing/godoxy/internal/health/monitor"
|
"github.com/yusing/godoxy/internal/health/monitor"
|
||||||
"github.com/yusing/godoxy/internal/idlewatcher"
|
"github.com/yusing/godoxy/internal/idlewatcher"
|
||||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
"github.com/yusing/godoxy/internal/route/stream"
|
"github.com/yusing/godoxy/internal/route/stream"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
@@ -26,6 +25,8 @@ type StreamRoute struct {
|
|||||||
l zerolog.Logger
|
l zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ types.StreamRoute = (*StreamRoute)(nil)
|
||||||
|
|
||||||
func NewStreamRoute(base *Route) (types.Route, gperr.Error) {
|
func NewStreamRoute(base *Route) (types.Route, gperr.Error) {
|
||||||
// TODO: support non-coherent scheme
|
// TODO: support non-coherent scheme
|
||||||
return &StreamRoute{Route: base}, nil
|
return &StreamRoute{Route: base}, nil
|
||||||
@@ -65,31 +66,25 @@ func (r *StreamRoute) Start(parent task.Parent) gperr.Error {
|
|||||||
if r.HealthMon != nil {
|
if r.HealthMon != nil {
|
||||||
if err := r.HealthMon.Start(r.task); err != nil {
|
if err := r.HealthMon.Start(r.task); err != nil {
|
||||||
gperr.LogWarn("health monitor error", err, &r.l)
|
gperr.LogWarn("health monitor error", err, &r.l)
|
||||||
|
r.HealthMon = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ListenAndServe(r.task.Context(), nil, nil)
|
ep := entrypoint.FromCtx(parent.Context())
|
||||||
r.l = log.With().
|
if ep == nil {
|
||||||
Str("type", r.LisURL.Scheme+"->"+r.ProxyURL.Scheme).
|
err := gperr.New("entrypoint not initialized")
|
||||||
Str("name", r.Name()).
|
r.task.Finish(err)
|
||||||
Stringer("rurl", r.ProxyURL).
|
return err
|
||||||
Stringer("laddr", r.LocalAddr()).Logger()
|
}
|
||||||
r.l.Info().Msg("stream started")
|
if err := ep.StartAddRoute(r); err != nil {
|
||||||
|
r.task.Finish(err)
|
||||||
r.task.OnCancel("close_stream", func() {
|
return gperr.Wrap(err)
|
||||||
r.stream.Close()
|
}
|
||||||
r.l.Info().Msg("stream closed")
|
|
||||||
})
|
|
||||||
|
|
||||||
routes.Stream.Add(r)
|
|
||||||
r.task.OnCancel("remove_route_from_stream", func() {
|
|
||||||
routes.Stream.Del(r)
|
|
||||||
})
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StreamRoute) ListenAndServe(ctx context.Context, preDial, onRead nettypes.HookFunc) {
|
func (r *StreamRoute) ListenAndServe(ctx context.Context, preDial, onRead nettypes.HookFunc) error {
|
||||||
r.stream.ListenAndServe(ctx, preDial, onRead)
|
return r.stream.ListenAndServe(ctx, preDial, onRead)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StreamRoute) Close() error {
|
func (r *StreamRoute) Close() error {
|
||||||
|
|||||||
@@ -63,10 +63,9 @@ func NewUDPUDPStream(network, listenAddr, dstAddr string) (nettypes.Stream, erro
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
type Stream interface {
|
type Stream interface {
|
||||||
ListenAndServe(ctx context.Context, preDial, onRead HookFunc)
|
ListenAndServe(ctx context.Context, preDial, onRead HookFunc) error
|
||||||
Close() error
|
Close() error
|
||||||
LocalAddr() net.Addr
|
LocalAddr() net.Addr
|
||||||
zerolog.LogObjectMarshaler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type HookFunc func(ctx context.Context) error
|
type HookFunc func(ctx context.Context) error
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"github.com/pires/go-proxyproto"
|
"github.com/pires/go-proxyproto"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/acl"
|
acl "github.com/yusing/godoxy/internal/acl/types"
|
||||||
"github.com/yusing/godoxy/internal/agentpool"
|
"github.com/yusing/godoxy/internal/agentpool"
|
||||||
"github.com/yusing/godoxy/internal/entrypoint"
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||||
ioutils "github.com/yusing/goutils/io"
|
ioutils "github.com/yusing/goutils/io"
|
||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
@@ -43,26 +43,28 @@ func NewTCPTCPStream(network, dstNetwork, listenAddr, dstAddr string, agent *age
|
|||||||
return &TCPTCPStream{network: network, dstNetwork: dstNetwork, laddr: laddr, dst: dst, agent: agent}, nil
|
return &TCPTCPStream{network: network, dstNetwork: dstNetwork, laddr: laddr, dst: dst, agent: agent}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TCPTCPStream) ListenAndServe(ctx context.Context, preDial, onRead nettypes.HookFunc) {
|
func (s *TCPTCPStream) ListenAndServe(ctx context.Context, preDial, onRead nettypes.HookFunc) error {
|
||||||
var err error
|
var err error
|
||||||
s.listener, err = net.ListenTCP(s.network, s.laddr)
|
s.listener, err = net.ListenTCP(s.network, s.laddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logErr(s, err, "failed to listen")
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if acl, ok := ctx.Value(acl.ContextKey{}).(*acl.Config); ok {
|
if ep := entrypoint.FromCtx(ctx); ep != nil {
|
||||||
|
if proxyProto := ep.SupportProxyProtocol(); proxyProto {
|
||||||
|
s.listener = &proxyproto.Listener{Listener: s.listener}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if acl := acl.FromCtx(ctx); acl != nil {
|
||||||
log.Debug().Str("listener", s.listener.Addr().String()).Msg("wrapping listener with ACL")
|
log.Debug().Str("listener", s.listener.Addr().String()).Msg("wrapping listener with ACL")
|
||||||
s.listener = acl.WrapTCP(s.listener)
|
s.listener = acl.WrapTCP(s.listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
if proxyProto := entrypoint.ActiveConfig.Load().SupportProxyProtocol; proxyProto {
|
|
||||||
s.listener = &proxyproto.Listener{Listener: s.listener}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.preDial = preDial
|
s.preDial = preDial
|
||||||
s.onRead = onRead
|
s.onRead = onRead
|
||||||
go s.listen(ctx)
|
go s.listen(ctx)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TCPTCPStream) Close() error {
|
func (s *TCPTCPStream) Close() error {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/acl"
|
acl "github.com/yusing/godoxy/internal/acl/types"
|
||||||
"github.com/yusing/godoxy/internal/agentpool"
|
"github.com/yusing/godoxy/internal/agentpool"
|
||||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||||
"github.com/yusing/goutils/synk"
|
"github.com/yusing/goutils/synk"
|
||||||
@@ -75,14 +75,13 @@ func NewUDPUDPStream(network, dstNetwork, listenAddr, dstAddr string, agent *age
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UDPUDPStream) ListenAndServe(ctx context.Context, preDial, onRead nettypes.HookFunc) {
|
func (s *UDPUDPStream) ListenAndServe(ctx context.Context, preDial, onRead nettypes.HookFunc) error {
|
||||||
l, err := net.ListenUDP(s.network, s.laddr)
|
l, err := net.ListenUDP(s.network, s.laddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logErr(s, err, "failed to listen")
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
s.listener = l
|
s.listener = l
|
||||||
if acl, ok := ctx.Value(acl.ContextKey{}).(*acl.Config); ok {
|
if acl := acl.FromCtx(ctx); acl != nil {
|
||||||
log.Debug().Str("listener", s.listener.LocalAddr().String()).Msg("wrapping listener with ACL")
|
log.Debug().Str("listener", s.listener.LocalAddr().String()).Msg("wrapping listener with ACL")
|
||||||
s.listener = acl.WrapUDP(s.listener)
|
s.listener = acl.WrapUDP(s.listener)
|
||||||
}
|
}
|
||||||
@@ -90,6 +89,7 @@ func (s *UDPUDPStream) ListenAndServe(ctx context.Context, preDial, onRead netty
|
|||||||
s.onRead = onRead
|
s.onRead = onRead
|
||||||
go s.listen(ctx)
|
go s.listen(ctx)
|
||||||
go s.cleanUp(ctx)
|
go s.cleanUp(ctx)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UDPUDPStream) Close() error {
|
func (s *UDPUDPStream) Close() error {
|
||||||
|
|||||||
32
internal/route/test_route.go
Normal file
32
internal/route/test_route.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/yusing/godoxy/internal/entrypoint"
|
||||||
|
epctx "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
"github.com/yusing/goutils/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewStartedTestRoute(t testing.TB, base *Route) (types.Route, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
task := task.GetTestTask(t)
|
||||||
|
if ep := epctx.FromCtx(task.Context()); ep == nil {
|
||||||
|
ep = entrypoint.NewEntrypoint(task, nil)
|
||||||
|
epctx.SetCtx(task, ep)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := base.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = base.Start(task)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.impl, nil
|
||||||
|
}
|
||||||
@@ -74,6 +74,19 @@ type (
|
|||||||
Config *LoadBalancerConfig `json:"config"`
|
Config *LoadBalancerConfig `json:"config"`
|
||||||
Pool map[string]any `json:"pool"`
|
Pool map[string]any `json:"pool"`
|
||||||
} // @name HealthExtra
|
} // @name HealthExtra
|
||||||
|
|
||||||
|
HealthInfoWithoutDetail struct {
|
||||||
|
Status HealthStatus `json:"status" swaggertype:"string" enums:"healthy,unhealthy,napping,starting,error,unknown"`
|
||||||
|
Uptime time.Duration `json:"uptime" swaggertype:"number"` // uptime in milliseconds
|
||||||
|
Latency time.Duration `json:"latency" swaggertype:"number"` // latency in microseconds
|
||||||
|
} // @name HealthInfoWithoutDetail
|
||||||
|
|
||||||
|
HealthInfo struct {
|
||||||
|
HealthInfoWithoutDetail
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
} // @name HealthInfo
|
||||||
|
|
||||||
|
HealthMap = map[string]HealthStatusString // @name HealthMap
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type (
|
|||||||
pool.Object
|
pool.Object
|
||||||
ProviderName() string
|
ProviderName() string
|
||||||
GetProvider() RouteProvider
|
GetProvider() RouteProvider
|
||||||
|
ListenURL() *nettypes.URL
|
||||||
TargetURL() *nettypes.URL
|
TargetURL() *nettypes.URL
|
||||||
HealthMonitor() HealthMonitor
|
HealthMonitor() HealthMonitor
|
||||||
SetHealthMonitor(m HealthMonitor)
|
SetHealthMonitor(m HealthMonitor)
|
||||||
|
|||||||
Reference in New Issue
Block a user