mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 16:58:31 +02:00
refactor(entrypoint): move route registry into entrypoint context
Replace global routes registry with entrypoint-scoped pools and context lookups, and centralize API/metrics startup in config state.
This commit is contained in:
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
goutils
2
goutils
Submodule goutils updated: 52ea531e95...a270ef85af
@@ -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 {
|
||||||
|
|||||||
@@ -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,8 @@ 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)
|
||||||
|
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,7 +5,7 @@ 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/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
|
|
||||||
@@ -24,11 +24,12 @@ import (
|
|||||||
// @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 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,8 +4,8 @@ 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"
|
||||||
)
|
)
|
||||||
@@ -21,15 +21,16 @@ import (
|
|||||||
// @Failure 403 {object} apitypes.ErrorResponse
|
// @Failure 403 {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())
|
||||||
|
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"
|
||||||
@@ -53,29 +53,30 @@ func Items(c *gin.Context) {
|
|||||||
hostname = host
|
hostname = host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ep := entrypoint.FromCtx(c.Request.Context())
|
||||||
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,8 +4,8 @@ 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"
|
_ "github.com/yusing/goutils/apitypes"
|
||||||
)
|
)
|
||||||
@@ -24,5 +24,6 @@ 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())
|
||||||
|
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,8 @@ func Route(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
route, ok := routes.GetIncludeExcluded(request.Which)
|
ep := entrypoint.FromCtx(c.Request.Context())
|
||||||
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,49 +1,131 @@
|
|||||||
package entrypoint
|
package entrypoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
"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 *entrypoint.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
|
||||||
|
tcpListeners *xsync.Map[string, net.Listener] // listen addr -> listener
|
||||||
|
udpListeners *xsync.Map[string, net.PacketConn] // listen addr -> listener
|
||||||
}
|
}
|
||||||
|
|
||||||
// nil-safe
|
var _ entrypoint.Entrypoint = &Entrypoint{}
|
||||||
var ActiveConfig atomic.Pointer[entrypoint.Config]
|
|
||||||
|
|
||||||
func init() {
|
var emptyCfg entrypoint.Config
|
||||||
// make sure it's not nil
|
|
||||||
ActiveConfig.Store(&entrypoint.Config{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEntrypoint() Entrypoint {
|
func NewEntrypoint(parent task.Parent, cfg *entrypoint.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](),
|
||||||
|
tcpListeners: xsync.NewMap[string, net.Listener](),
|
||||||
|
udpListeners: xsync.NewMap[string, net.PacketConn](),
|
||||||
|
}
|
||||||
|
ep.task.OnCancel("stop", func() {
|
||||||
|
// servers stop on their own when context is cancelled
|
||||||
|
var errs gperr.Group
|
||||||
|
for _, listener := range ep.tcpListeners.Range {
|
||||||
|
errs.Go(func() error {
|
||||||
|
return listener.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, listener := range ep.udpListeners.Range {
|
||||||
|
errs.Go(func() error {
|
||||||
|
return listener.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := errs.Wait().Error(); err != nil {
|
||||||
|
gperr.LogError("failed to stop entrypoint listeners", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ep.task.OnFinished("cleanup", func() {
|
||||||
|
ep.servers.Clear()
|
||||||
|
ep.tcpListeners.Clear()
|
||||||
|
ep.udpListeners.Clear()
|
||||||
|
})
|
||||||
|
return ep
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ep *Entrypoint) ShortLinkMatcher() *ShortLinkMatcher {
|
func (ep *Entrypoint) ShortLinkMatcher() *ShortLinkMatcher {
|
||||||
return ep.shortLinkMatcher
|
return ep.shortLinkMatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) Config() *entrypoint.Config {
|
||||||
|
return ep.cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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) 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 +156,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 +173,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
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
|
|
||||||
. "github.com/yusing/godoxy/internal/entrypoint"
|
. "github.com/yusing/godoxy/internal/entrypoint"
|
||||||
"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"
|
||||||
@@ -90,16 +89,21 @@ func BenchmarkEntrypointReal(b *testing.B) {
|
|||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.Start(task.RootTask("test", false))
|
err = r.Start(task.NewTestTask(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var w noopResponseWriter
|
var w noopResponseWriter
|
||||||
|
|
||||||
|
server, ok := ep.GetServer(r.ListenURL().Host)
|
||||||
|
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)
|
||||||
// }
|
// }
|
||||||
@@ -140,7 +144,7 @@ func BenchmarkEntrypoint(b *testing.B) {
|
|||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rev, ok := routes.HTTP.Get("test")
|
rev, ok := ep.HTTPRoutes().Get("test")
|
||||||
if !ok {
|
if !ok {
|
||||||
b.Fatal("route not found")
|
b.Fatal("route not found")
|
||||||
}
|
}
|
||||||
@@ -148,9 +152,14 @@ func BenchmarkEntrypoint(b *testing.B) {
|
|||||||
|
|
||||||
var w noopResponseWriter
|
var w noopResponseWriter
|
||||||
|
|
||||||
|
server, ok := ep.GetServer(r.ListenURL().Host)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ import (
|
|||||||
|
|
||||||
. "github.com/yusing/godoxy/internal/entrypoint"
|
. "github.com/yusing/godoxy/internal/entrypoint"
|
||||||
"github.com/yusing/godoxy/internal/route"
|
"github.com/yusing/godoxy/internal/route"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
|
||||||
|
|
||||||
|
"github.com/yusing/goutils/task"
|
||||||
expect "github.com/yusing/goutils/testing"
|
expect "github.com/yusing/goutils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ep = NewEntrypoint()
|
func addRoute(ep *Entrypoint, alias string) {
|
||||||
|
ep.AddRoute(&route.ReveseProxyRoute{
|
||||||
func addRoute(alias string) {
|
|
||||||
routes.HTTP.Add(&route.ReveseProxyRoute{
|
|
||||||
Route: &route.Route{
|
Route: &route.Route{
|
||||||
Alias: alias,
|
Alias: alias,
|
||||||
Port: route.Port{
|
Port: route.Port{
|
||||||
@@ -25,26 +23,28 @@ func addRoute(alias string) {
|
|||||||
|
|
||||||
func run(t *testing.T, match []string, noMatch []string) {
|
func run(t *testing.T, match []string, noMatch []string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Cleanup(routes.Clear)
|
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||||
t.Cleanup(func() { ep.SetFindRouteDomains(nil) })
|
|
||||||
|
|
||||||
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)
|
found, ok := ep.HTTPRoutes().Get(test)
|
||||||
|
expect.True(t, ok)
|
||||||
expect.NotNil(t, found)
|
expect.NotNil(t, found)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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.False(t, ok)
|
||||||
expect.Nil(t, found)
|
expect.Nil(t, found)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindRouteAnyDomain(t *testing.T) {
|
func TestFindRouteAnyDomain(t *testing.T) {
|
||||||
addRoute("app1")
|
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||||
|
addRoute(ep, "app1")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1.com",
|
"app1.com",
|
||||||
@@ -62,6 +62,7 @@ func TestFindRouteAnyDomain(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFindRouteExactHostMatch(t *testing.T) {
|
func TestFindRouteExactHostMatch(t *testing.T) {
|
||||||
|
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app2.com",
|
"app2.com",
|
||||||
"app2.domain.com",
|
"app2.domain.com",
|
||||||
@@ -75,19 +76,20 @@ func TestFindRouteExactHostMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
addRoute(test)
|
addRoute(ep, test)
|
||||||
}
|
}
|
||||||
|
|
||||||
run(t, tests, testsNoMatch)
|
run(t, tests, testsNoMatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindRouteByDomains(t *testing.T) {
|
func TestFindRouteByDomains(t *testing.T) {
|
||||||
|
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||||
ep.SetFindRouteDomains([]string{
|
ep.SetFindRouteDomains([]string{
|
||||||
".domain.com",
|
".domain.com",
|
||||||
".sub.domain.com",
|
".sub.domain.com",
|
||||||
})
|
})
|
||||||
|
|
||||||
addRoute("app1")
|
addRoute(ep, "app1")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1.domain.com",
|
"app1.domain.com",
|
||||||
@@ -107,12 +109,13 @@ func TestFindRouteByDomains(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFindRouteByDomainsExactMatch(t *testing.T) {
|
func TestFindRouteByDomainsExactMatch(t *testing.T) {
|
||||||
|
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||||
ep.SetFindRouteDomains([]string{
|
ep.SetFindRouteDomains([]string{
|
||||||
".domain.com",
|
".domain.com",
|
||||||
".sub.domain.com",
|
".sub.domain.com",
|
||||||
})
|
})
|
||||||
|
|
||||||
addRoute("app1.foo.bar")
|
addRoute(ep, "app1.foo.bar")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1.foo.bar", // exact match
|
"app1.foo.bar", // exact match
|
||||||
@@ -131,8 +134,9 @@ func TestFindRouteByDomainsExactMatch(t *testing.T) {
|
|||||||
|
|
||||||
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 := NewEntrypoint(task.NewTestTask(t), nil)
|
||||||
addRoute("app2.com")
|
addRoute(ep, "app1")
|
||||||
|
addRoute(ep, "app2.com")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1:8080",
|
"app1:8080",
|
||||||
@@ -148,12 +152,13 @@ func TestFindRouteWithPort(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ByDomains", func(t *testing.T) {
|
t.Run("ByDomains", func(t *testing.T) {
|
||||||
|
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||||
ep.SetFindRouteDomains([]string{
|
ep.SetFindRouteDomains([]string{
|
||||||
".domain.com",
|
".domain.com",
|
||||||
})
|
})
|
||||||
addRoute("app1")
|
addRoute(ep, "app1")
|
||||||
addRoute("app2")
|
addRoute(ep, "app2")
|
||||||
addRoute("app3.domain.com")
|
addRoute(ep, "app3.domain.com")
|
||||||
|
|
||||||
tests := []string{
|
tests := []string{
|
||||||
"app1.domain.com:8080",
|
"app1.domain.com:8080",
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
172
internal/entrypoint/http_server.go
Normal file
172
internal/entrypoint/http_server.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
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 {
|
||||||
|
srv *server.Server
|
||||||
|
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.srv != nil {
|
||||||
|
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)
|
||||||
|
server, err := server.StartServer(task, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv.stopFunc = task.FinishAndWait
|
||||||
|
srv.addr = addr
|
||||||
|
srv.srv = server
|
||||||
|
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() {
|
||||||
|
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.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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()
|
||||||
|
}
|
||||||
111
internal/entrypoint/routes.go
Normal file
111
internal/entrypoint/routes.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"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) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, r := range ep.streamRoutes.Iter {
|
||||||
|
if !yield(r) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, r := range ep.excludedRoutes.Iter {
|
||||||
|
if !yield(r) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) AddRoute(r types.Route) {
|
||||||
|
if r.ShouldExclude() {
|
||||||
|
ep.excludedRoutes.Add(r)
|
||||||
|
r.Task().OnCancel("remove_route", func() {
|
||||||
|
ep.excludedRoutes.Del(r)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch r := r.(type) {
|
||||||
|
case types.HTTPRoute:
|
||||||
|
ep.AddHTTPRoute(r)
|
||||||
|
ep.shortLinkMatcher.AddRoute(r.Key())
|
||||||
|
r.Task().OnCancel("remove_route", func() {
|
||||||
|
ep.delHTTPRoute(r)
|
||||||
|
ep.shortLinkMatcher.DelRoute(r.Key())
|
||||||
|
})
|
||||||
|
case types.StreamRoute:
|
||||||
|
ep.streamRoutes.Add(r)
|
||||||
|
r.Task().OnCancel("remove_route", func() {
|
||||||
|
ep.streamRoutes.Del(r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if port := route.ListenURL().Port(); port == "" || port == "0" {
|
||||||
|
host := route.ListenURL().Hostname()
|
||||||
|
if host == "" {
|
||||||
|
host = common.ProxyHTTPHost
|
||||||
|
}
|
||||||
|
httpAddr := net.JoinHostPort(host, strconv.Itoa(common.ProxyHTTPPort))
|
||||||
|
httpsAddr := net.JoinHostPort(host, strconv.Itoa(common.ProxyHTTPSPort))
|
||||||
|
return errors.Join(ep.addHTTPRoute(route, httpAddr, HTTPProtoHTTP), ep.addHTTPRoute(route, httpsAddr, HTTPProtoHTTPS))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ep.addHTTPRoute(route, route.ListenURL().Host, HTTPProtoHTTPS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) addHTTPRoute(route types.HTTPRoute, addr string, proto HTTPProto) error {
|
||||||
|
var err error
|
||||||
|
srv, _ := ep.servers.LoadOrCompute(addr, func() (srv *httpServer, cancel bool) {
|
||||||
|
srv = newHTTPServer(ep)
|
||||||
|
err = srv.Listen(addr, proto)
|
||||||
|
cancel = err != nil
|
||||||
|
return
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.AddRoute(route)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) delHTTPRoute(route types.HTTPRoute) {
|
||||||
|
addr := route.ListenURL().Host
|
||||||
|
srv, _ := ep.servers.Load(addr)
|
||||||
|
if srv != nil {
|
||||||
|
srv.DelRoute(route)
|
||||||
|
}
|
||||||
|
// TODO: close if no servers left
|
||||||
|
}
|
||||||
@@ -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.NewTestTask(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.NewTestTask(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.NewTestTask(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.NewTestTask(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.NewTestTask(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.NewTestTask(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:8080", 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 {
|
||||||
|
Config() *Config
|
||||||
|
|
||||||
|
DisablePoolsLog(v bool)
|
||||||
|
|
||||||
|
GetRoute(alias string) (types.Route, bool)
|
||||||
|
AddRoute(r types.Route)
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ import (
|
|||||||
|
|
||||||
"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"`
|
||||||
@@ -41,7 +42,7 @@ var Poller = period.NewPoller("uptime", getStatuses, aggregateStatuses)
|
|||||||
|
|
||||||
func getStatuses(ctx context.Context, _ StatusByAlias) (StatusByAlias, error) {
|
func getStatuses(ctx context.Context, _ StatusByAlias) (StatusByAlias, error) {
|
||||||
return StatusByAlias{
|
return StatusByAlias{
|
||||||
Map: routes.GetHealthInfoWithoutDetail(),
|
Map: entrypoint.FromCtx(ctx).GetHealthInfoWithoutDetail(),
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -127,11 +128,13 @@ 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 {
|
r, ok := entrypoint.FromCtx(state.Context()).GetRoute(alias)
|
||||||
mon := r.HealthMonitor()
|
if ok {
|
||||||
if mon != nil {
|
mon := r.HealthMonitor()
|
||||||
status = mon.Status()
|
if mon != nil {
|
||||||
|
status = mon.Status()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ func TestEntrypointBypassRoute(t *testing.T) {
|
|||||||
expect.NoError(t, err)
|
expect.NoError(t, err)
|
||||||
|
|
||||||
expect.NoError(t, err)
|
expect.NoError(t, err)
|
||||||
entry := entrypoint.NewEntrypoint()
|
entry := entrypoint.NewEntrypoint(task.NewTestTask(t), nil)
|
||||||
r := &route.Route{
|
r := &route.Route{
|
||||||
Alias: "test-route",
|
Alias: "test-route",
|
||||||
Host: host,
|
Host: host,
|
||||||
@@ -260,7 +260,11 @@ func TestEntrypointBypassRoute(t *testing.T) {
|
|||||||
|
|
||||||
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(r.ListenURL().Host)
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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,13 @@ 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)
|
entrypoint.FromCtx(parent.Context()).AddRoute(s)
|
||||||
if state := config.WorkingState.Load(); state != nil {
|
|
||||||
state.ShortLinkMatcher().AddRoute(s.Alias)
|
|
||||||
}
|
|
||||||
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,15 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.UseLoadBalance() {
|
if r.UseLoadBalance() {
|
||||||
r.addToLoadBalancer(parent)
|
r.addToLoadBalancer(parent)
|
||||||
} else {
|
} else {
|
||||||
routes.HTTP.Add(r)
|
entrypoint.FromCtx(parent.Context()).AddRoute(r)
|
||||||
if state := config.WorkingState.Load(); state != nil {
|
|
||||||
state.ShortLinkMatcher().AddRoute(r.Alias)
|
|
||||||
}
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -192,7 +183,8 @@ func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
|
|||||||
cfg := r.LoadBalance
|
cfg := r.LoadBalance
|
||||||
lbLock.Lock()
|
lbLock.Lock()
|
||||||
|
|
||||||
l, ok := routes.HTTP.Get(cfg.Link)
|
ep := entrypoint.FromCtx(r.task.Context())
|
||||||
|
l, ok := ep.HTTPRoutes().Get(cfg.Link)
|
||||||
var linked *ReveseProxyRoute
|
var linked *ReveseProxyRoute
|
||||||
if ok {
|
if ok {
|
||||||
lbLock.Unlock()
|
lbLock.Unlock()
|
||||||
@@ -214,16 +206,7 @@ func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
|
|||||||
handler: lb,
|
handler: lb,
|
||||||
}
|
}
|
||||||
linked.SetHealthMonitor(lb)
|
linked.SetHealthMonitor(lb)
|
||||||
routes.HTTP.AddKey(cfg.Link, linked)
|
ep.AddRoute(linked)
|
||||||
if state := config.WorkingState.Load(); state != nil {
|
|
||||||
state.ShortLinkMatcher().AddRoute(cfg.Link)
|
|
||||||
}
|
|
||||||
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()
|
lbLock.Unlock()
|
||||||
}
|
}
|
||||||
r.loadBalancer = lb
|
r.loadBalancer = lb
|
||||||
|
|||||||
@@ -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,8 +46,7 @@ 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,dive,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"`
|
||||||
SPA bool `json:"spa,omitempty"` // Single-page app mode: serves index for non-existent paths
|
SPA bool `json:"spa,omitempty"` // Single-page app mode: serves index for non-existent paths
|
||||||
@@ -274,24 +273,17 @@ 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.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.SchemeHTTP, route.SchemeHTTPS, route.SchemeH2C:
|
||||||
if r.Bind == "" {
|
r.LisURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("https://%s", net.JoinHostPort(r.Bind, strconv.Itoa(r.Port.Listening))))
|
||||||
r.Bind = "0.0.0.0"
|
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:
|
||||||
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 {
|
||||||
@@ -317,6 +309,12 @@ func (r *Route) validate() gperr.Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.Scheme == route.SchemeFileServer {
|
||||||
|
r.Host = ""
|
||||||
|
r.Port.Proxy = 0
|
||||||
|
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, "file://"+r.Root)
|
||||||
|
}
|
||||||
|
|
||||||
if !r.UseHealthCheck() && (r.UseLoadBalance() || r.UseIdleWatcher()) {
|
if !r.UseHealthCheck() && (r.UseLoadBalance() || r.UseIdleWatcher()) {
|
||||||
errs.Adds("cannot disable healthcheck when loadbalancer or idle watcher is enabled")
|
errs.Adds("cannot disable healthcheck when loadbalancer or idle watcher is enabled")
|
||||||
}
|
}
|
||||||
@@ -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,19 @@ 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)
|
|
||||||
|
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 +566,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 +938,20 @@ func (r *Route) Finalize() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch r.Scheme {
|
||||||
|
case route.SchemeTCP, route.SchemeUDP:
|
||||||
|
if r.Bind == "" {
|
||||||
|
r.Bind = "0.0.0.0"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if r.Bind == "" {
|
||||||
|
r.Bind = common.ProxyHTTPSHost
|
||||||
|
}
|
||||||
|
if r.Port.Proxy == 0 {
|
||||||
|
r.Port.Proxy = common.ProxyHTTPSPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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,6 +962,7 @@ func (r *Route) Finalize() {
|
|||||||
panic("bug: working state is nil")
|
panic("bug: working state is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: default value from context
|
||||||
r.HealthCheck.ApplyDefaults(config.WorkingState.Load().Value().Defaults.HealthCheck)
|
r.HealthCheck.ApplyDefaults(config.WorkingState.Load().Value().Defaults.HealthCheck)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
nettypes "github.com/yusing/godoxy/internal/net/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/route/routes"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
httputils "github.com/yusing/goutils/http"
|
httputils "github.com/yusing/goutils/http"
|
||||||
"github.com/yusing/goutils/http/reverseproxy"
|
"github.com/yusing/goutils/http/reverseproxy"
|
||||||
@@ -195,20 +194,23 @@ var commands = map[string]struct {
|
|||||||
return args[0], nil
|
return args[0], nil
|
||||||
},
|
},
|
||||||
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)
|
|
||||||
if !ok {
|
// FIXME: circular dependency
|
||||||
excluded, has := routes.Excluded.Get(route)
|
// ep := entrypoint.FromCtx(req.Context())
|
||||||
if has {
|
// r, ok := ep.HTTPRoutes().Get(route)
|
||||||
r, ok = excluded.(types.HTTPRoute)
|
// if !ok {
|
||||||
}
|
// excluded, has := ep.ExcludedRoutes().Get(route)
|
||||||
}
|
// if has {
|
||||||
if ok {
|
// r, ok = excluded.(types.HTTPRoute)
|
||||||
r.ServeHTTP(w, req)
|
// }
|
||||||
} else {
|
// }
|
||||||
http.Error(w, fmt.Sprintf("Route %q not found", route), http.StatusNotFound)
|
// if ok {
|
||||||
}
|
// r.ServeHTTP(w, req)
|
||||||
|
// } else {
|
||||||
|
// http.Error(w, fmt.Sprintf("Route %q not found", route), http.StatusNotFound)
|
||||||
|
// }
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"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"
|
||||||
@@ -65,6 +65,7 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,10 +82,7 @@ func (r *StreamRoute) Start(parent task.Parent) gperr.Error {
|
|||||||
r.l.Info().Msg("stream closed")
|
r.l.Info().Msg("stream closed")
|
||||||
})
|
})
|
||||||
|
|
||||||
routes.Stream.Add(r)
|
entrypoint.FromCtx(parent.Context()).AddRoute(r)
|
||||||
r.task.OnCancel("remove_route_from_stream", func() {
|
|
||||||
routes.Stream.Del(r)
|
|
||||||
})
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -51,12 +51,14 @@ func (s *TCPTCPStream) ListenAndServe(ctx context.Context, preDial, onRead netty
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if acl, ok := ctx.Value(acl.ContextKey{}).(*acl.Config); ok {
|
// TODO: add to entrypoint
|
||||||
|
|
||||||
|
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 {
|
if proxyProto := entrypoint.FromCtx(ctx).Config().SupportProxyProtocol; proxyProto {
|
||||||
s.listener = &proxyproto.Listener{Listener: s.listener}
|
s.listener = &proxyproto.Listener{Listener: s.listener}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -82,10 +82,11 @@ func (s *UDPUDPStream) ListenAndServe(ctx context.Context, preDial, onRead netty
|
|||||||
return
|
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)
|
||||||
}
|
}
|
||||||
|
// TODO: add to entrypoint
|
||||||
s.preDial = preDial
|
s.preDial = preDial
|
||||||
s.onRead = onRead
|
s.onRead = onRead
|
||||||
go s.listen(ctx)
|
go s.listen(ctx)
|
||||||
|
|||||||
@@ -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