mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-07 11:09:32 +01:00
Compare commits
32 Commits
refactor/e
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bb36e2e83 | ||
|
|
4b57ef1cad | ||
|
|
3850a4a6e7 | ||
|
|
da3c624582 | ||
|
|
157a83bef8 | ||
|
|
d61bd5ce51 | ||
|
|
bad3e9a989 | ||
|
|
9adfd73121 | ||
|
|
4a652aaf55 | ||
|
|
16c986978d | ||
|
|
107b7c5f64 | ||
|
|
818d75c8b7 | ||
|
|
f1bc5de3ea | ||
|
|
425ff0b25c | ||
|
|
1f6614e337 | ||
|
|
9ba102a33d | ||
|
|
31c616246b | ||
|
|
390859bd1f | ||
|
|
243662c13b | ||
|
|
588e9f5b18 | ||
|
|
a3bf88cc9c | ||
|
|
9b1af57859 | ||
|
|
bb7471cc9c | ||
|
|
a403b2b629 | ||
|
|
54b9e7f236 | ||
|
|
45b89cd452 | ||
|
|
72fea96c7b | ||
|
|
aef646be6f | ||
|
|
135a4ff6c7 | ||
|
|
5f418b62c7 | ||
|
|
bd92c46375 | ||
|
|
21a23dd147 |
@@ -1,5 +1,5 @@
|
||||
# Stage 1: deps
|
||||
FROM golang:1.25.7-alpine AS deps
|
||||
FROM golang:1.25.6-alpine AS deps
|
||||
HEALTHCHECK NONE
|
||||
|
||||
# package version does not matter
|
||||
|
||||
2
Makefile
2
Makefile
@@ -92,7 +92,7 @@ docker-build-test:
|
||||
|
||||
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
|
||||
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
|
||||
gomod_paths := $(shell find . -name go.mod -type f | grep -vE '^./internal/(go-oidc|go-proxmox|gopsutil)/' | xargs dirname)
|
||||
gomod_paths := $(shell find . -name go.mod -type f | xargs dirname)
|
||||
|
||||
update-go:
|
||||
for file in ${files}; do \
|
||||
|
||||
@@ -161,7 +161,7 @@ Tips:
|
||||
srv.Serve(l)
|
||||
}
|
||||
|
||||
systeminfo.Poller.Start(t)
|
||||
systeminfo.Poller.Start()
|
||||
|
||||
task.WaitExit(3)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/yusing/godoxy/agent
|
||||
|
||||
go 1.25.7
|
||||
go 1.25.6
|
||||
|
||||
exclude (
|
||||
github.com/moby/moby/api v1.53.0 // allow older daemon versions
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.7-alpine AS builder
|
||||
FROM golang:1.25.6-alpine AS builder
|
||||
|
||||
HEALTHCHECK NONE
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module github.com/yusing/godoxy/cmd/bench_server
|
||||
|
||||
go 1.25.7
|
||||
go 1.25.6
|
||||
|
||||
@@ -181,6 +181,7 @@ func newApiHandler(debugMux *debugMux) *gin.Engine {
|
||||
registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon)
|
||||
registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health)
|
||||
registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons)
|
||||
registerGinRoute(v1, "POST", "Config reload", "/reload", apiV1.Reload)
|
||||
registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats)
|
||||
|
||||
route := v1.Group("/route")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.7-alpine AS builder
|
||||
FROM golang:1.25.6-alpine AS builder
|
||||
|
||||
HEALTHCHECK NONE
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/yusing/godoxy/cmd/h2c_test_server
|
||||
|
||||
go 1.25.7
|
||||
go 1.25.6
|
||||
|
||||
require golang.org/x/net v0.49.0
|
||||
|
||||
|
||||
29
cmd/main.go
29
cmd/main.go
@@ -1,12 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/api"
|
||||
"github.com/yusing/godoxy/internal/auth"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/config"
|
||||
@@ -14,9 +14,12 @@ import (
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
"github.com/yusing/godoxy/internal/logging"
|
||||
"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/route/rules"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/server"
|
||||
"github.com/yusing/goutils/task"
|
||||
"github.com/yusing/goutils/version"
|
||||
)
|
||||
@@ -48,6 +51,7 @@ func main() {
|
||||
parallel(
|
||||
dnsproviders.InitProviders,
|
||||
iconlist.InitCache,
|
||||
systeminfo.Poller.Start,
|
||||
middleware.LoadComposeFiles,
|
||||
)
|
||||
|
||||
@@ -62,20 +66,35 @@ func main() {
|
||||
|
||||
err := config.Load()
|
||||
if err != nil {
|
||||
var criticalErr config.CriticalError
|
||||
if errors.As(err, &criticalErr) {
|
||||
gperr.LogFatal("critical error in config", criticalErr)
|
||||
}
|
||||
gperr.LogWarn("errors in config", err)
|
||||
}
|
||||
|
||||
config.StartProxyServers()
|
||||
|
||||
if err := auth.Initialize(); err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||
}
|
||||
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()
|
||||
|
||||
uptime.Poller.Start()
|
||||
config.WatchChanges()
|
||||
|
||||
close(done)
|
||||
|
||||
@@ -31,8 +31,8 @@ services:
|
||||
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:rw
|
||||
- /app/node_modules/.cache:rw
|
||||
- /app/.next/cache # next image caching
|
||||
|
||||
# for lite variant, do not change uid/gid
|
||||
# - /var/cache/nginx:uid=101,gid=101
|
||||
# - /run:uid=101,gid=101
|
||||
|
||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/yusing/godoxy
|
||||
|
||||
go 1.25.7
|
||||
go 1.25.6
|
||||
|
||||
exclude (
|
||||
github.com/moby/moby/api v1.53.0 // allow older daemon versions
|
||||
|
||||
2
goutils
2
goutils
Submodule goutils updated: a270ef85af...52ea531e95
@@ -74,6 +74,8 @@ type ipLog struct {
|
||||
allowed bool
|
||||
}
|
||||
|
||||
type ContextKey struct{}
|
||||
|
||||
const cacheTTL = 1 * time.Minute
|
||||
|
||||
func (c *checkCache) Expired() bool {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package acl
|
||||
|
||||
import "net"
|
||||
|
||||
type ACL interface {
|
||||
IPAllowed(ip net.IP) bool
|
||||
WrapTCP(l net.Listener) net.Listener
|
||||
WrapUDP(l net.PacketConn) net.PacketConn
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -76,6 +76,7 @@ func NewHandler(requireAuth bool) *gin.Engine {
|
||||
v1.GET("/favicon", apiV1.FavIcon)
|
||||
v1.GET("/health", apiV1.Health)
|
||||
v1.GET("/icons", apiV1.Icons)
|
||||
v1.POST("/reload", apiV1.Reload)
|
||||
v1.GET("/stats", apiV1.Stats)
|
||||
|
||||
route := v1.Group("/route")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -37,9 +36,6 @@ type VerifyNewAgentRequest struct {
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /agent/verify [post]
|
||||
func Verify(c *gin.Context) {
|
||||
// avoid timeout waiting for response headers
|
||||
c.Status(http.StatusContinue)
|
||||
|
||||
var request VerifyNewAgentRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
@@ -64,7 +60,7 @@ func Verify(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
nRoutesAdded, err := verifyNewAgent(c.Request.Context(), request.Host, ca, client, request.ContainerRuntime)
|
||||
nRoutesAdded, err := verifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
@@ -86,7 +82,7 @@ func Verify(c *gin.Context) {
|
||||
|
||||
var errAgentAlreadyExists = gperr.New("agent already exists")
|
||||
|
||||
func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
|
||||
func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
|
||||
var agentCfg agent.AgentConfig
|
||||
agentCfg.Addr = host
|
||||
agentCfg.Runtime = containerRuntime
|
||||
@@ -103,7 +99,7 @@ func verifyNewAgent(ctx context.Context, host string, ca agent.PEMPair, client a
|
||||
return 0, errAgentAlreadyExists
|
||||
}
|
||||
|
||||
err := agentCfg.InitWithCerts(ctx, ca.Cert, client.Cert, client.Key)
|
||||
err := agentCfg.InitWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
|
||||
if err != nil {
|
||||
return 0, gperr.Wrap(err, "failed to initialize agent config")
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
@@ -44,7 +44,7 @@ func Stats(c *gin.Context) {
|
||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
||||
if !ok {
|
||||
var route types.Route
|
||||
route, ok = entrypoint.FromCtx(c.Request.Context()).GetRoute(id)
|
||||
route, ok = routes.GetIncludeExcluded(id)
|
||||
if ok {
|
||||
cont := route.ContainerInfo()
|
||||
if cont == nil {
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
|
||||
_ "unsafe"
|
||||
@@ -73,11 +73,7 @@ func FavIcon(c *gin.Context) {
|
||||
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
|
||||
func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) {
|
||||
// try with route.Icon
|
||||
ep := entrypoint.FromCtx(ctx)
|
||||
if ep == nil { // impossible, but just in case
|
||||
return iconfetch.FetchResultWithErrorf(http.StatusInternalServerError, "entrypoint not initialized")
|
||||
}
|
||||
r, ok := ep.HTTPRoutes().Get(alias)
|
||||
r, ok := routes.HTTP.Get(alias)
|
||||
if !ok {
|
||||
return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
// @x-id "health"
|
||||
@@ -23,16 +24,11 @@ import (
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /health [get]
|
||||
func Health(c *gin.Context) {
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
if ep == nil { // impossible, but just in case
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
||||
return
|
||||
}
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) {
|
||||
return ep.GetHealthInfoSimple(), nil
|
||||
return routes.GetHealthInfoSimple(), nil
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, ep.GetHealthInfoSimple())
|
||||
c.JSON(http.StatusOK, routes.GetHealthInfoSimple())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
// @x-id "categories"
|
||||
@@ -22,20 +21,15 @@ import (
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/categories [get]
|
||||
func Categories(c *gin.Context) {
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
if ep == nil { // impossible, but just in case
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, HomepageCategories(ep))
|
||||
c.JSON(http.StatusOK, HomepageCategories())
|
||||
}
|
||||
|
||||
func HomepageCategories(ep entrypoint.Entrypoint) []string {
|
||||
func HomepageCategories() []string {
|
||||
check := make(map[string]struct{})
|
||||
categories := make([]string, 0)
|
||||
categories = append(categories, homepage.CategoryAll)
|
||||
categories = append(categories, homepage.CategoryFavorites)
|
||||
for _, r := range ep.HTTPRoutes().Iter {
|
||||
for _, r := range routes.HTTP.Iter {
|
||||
item := r.HomepageItem()
|
||||
if item.Category == "" {
|
||||
continue
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
@@ -53,30 +53,29 @@ func Items(c *gin.Context) {
|
||||
hostname = host
|
||||
}
|
||||
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) {
|
||||
return HomepageItems(ep, proto, hostname, &request), nil
|
||||
return HomepageItems(proto, hostname, &request), nil
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, HomepageItems(ep, proto, hostname, &request))
|
||||
c.JSON(http.StatusOK, HomepageItems(proto, hostname, &request))
|
||||
}
|
||||
}
|
||||
|
||||
func HomepageItems(ep entrypoint.Entrypoint, proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
|
||||
func HomepageItems(proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
|
||||
switch proto {
|
||||
case "http", "https":
|
||||
default:
|
||||
proto = "http"
|
||||
}
|
||||
|
||||
hp := homepage.NewHomepageMap(ep.HTTPRoutes().Size())
|
||||
hp := homepage.NewHomepageMap(routes.HTTP.Size())
|
||||
|
||||
if strings.Count(hostname, ".") > 1 {
|
||||
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
|
||||
}
|
||||
|
||||
for _, r := range ep.HTTPRoutes().Iter {
|
||||
for _, r := range routes.HTTP.Iter {
|
||||
if request.Provider != "" && r.ProviderName() != request.Provider {
|
||||
continue
|
||||
}
|
||||
|
||||
28
internal/api/v1/reload.go
Normal file
28
internal/api/v1/reload.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/config"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
// @x-id "reload"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Reload config
|
||||
// @Description Reload config
|
||||
// @Tags v1
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /reload [post]
|
||||
func Reload(c *gin.Context) {
|
||||
if err := config.Reload(); err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to reload config"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, apitypes.Success("config reloaded"))
|
||||
}
|
||||
@@ -4,11 +4,10 @@ import (
|
||||
"net/http"
|
||||
|
||||
"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/routes"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type RoutesByProvider map[string][]route.Route
|
||||
@@ -25,10 +24,5 @@ type RoutesByProvider map[string][]route.Route
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /route/by_provider [get]
|
||||
func ByProvider(c *gin.Context) {
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
if ep == nil { // impossible, but just in case
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, ep.RoutesByProvider())
|
||||
c.JSON(http.StatusOK, routes.ByProvider())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
@@ -32,13 +32,7 @@ func Route(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
if ep == nil { // impossible, but just in case
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("entrypoint not initialized"))
|
||||
return
|
||||
}
|
||||
|
||||
route, ok := ep.GetRoute(request.Which)
|
||||
route, ok := routes.GetIncludeExcluded(request.Which)
|
||||
if ok {
|
||||
c.JSON(http.StatusOK, route)
|
||||
return
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"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/routes"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
@@ -32,16 +32,14 @@ func Routes(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
|
||||
provider := c.Query("provider")
|
||||
if provider == "" {
|
||||
c.JSON(http.StatusOK, slices.Collect(ep.IterRoutes))
|
||||
c.JSON(http.StatusOK, slices.Collect(routes.IterAll))
|
||||
return
|
||||
}
|
||||
|
||||
rts := make([]types.Route, 0, ep.NumRoutes())
|
||||
for r := range ep.IterRoutes {
|
||||
rts := make([]types.Route, 0, routes.NumAllRoutes())
|
||||
for r := range routes.IterAll {
|
||||
if r.ProviderName() == provider {
|
||||
rts = append(rts, r)
|
||||
}
|
||||
@@ -50,19 +48,17 @@ func Routes(c *gin.Context) {
|
||||
}
|
||||
|
||||
func RoutesWS(c *gin.Context) {
|
||||
ep := entrypoint.FromCtx(c.Request.Context())
|
||||
|
||||
provider := c.Query("provider")
|
||||
if provider == "" {
|
||||
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
||||
return slices.Collect(ep.IterRoutes), nil
|
||||
return slices.Collect(routes.IterAll), nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
||||
rts := make([]types.Route, 0, ep.NumRoutes())
|
||||
for r := range ep.IterRoutes {
|
||||
rts := make([]types.Route, 0, routes.NumAllRoutes())
|
||||
for r := range routes.IterAll {
|
||||
if r.ProviderName() == provider {
|
||||
rts = append(rts, r)
|
||||
}
|
||||
|
||||
@@ -222,9 +222,8 @@ func (p *Provider) ObtainCertIfNotExistsAll() error {
|
||||
})
|
||||
}
|
||||
|
||||
err := errs.Wait().Error()
|
||||
p.rebuildSNIMatcher()
|
||||
return err
|
||||
return errs.Wait().Error()
|
||||
}
|
||||
|
||||
// obtainCertIfNotExists obtains a new certificate for this provider if it does not exist.
|
||||
@@ -262,10 +261,7 @@ func (p *Provider) ObtainCertAll() error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
err := errs.Wait().Error()
|
||||
p.rebuildSNIMatcher()
|
||||
return err
|
||||
return errs.Wait().Error()
|
||||
}
|
||||
|
||||
// ObtainCert renews existing certificate or obtains a new certificate for this provider.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
Setup() error
|
||||
GetCert(*tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||
ScheduleRenewalAll(task.Parent)
|
||||
ObtainCertAll() error
|
||||
|
||||
@@ -10,9 +10,11 @@ import (
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
config "github.com/yusing/godoxy/internal/config/types"
|
||||
"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/events"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/server"
|
||||
"github.com/yusing/goutils/strings/ansi"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
@@ -58,30 +60,20 @@ func Load() error {
|
||||
|
||||
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
|
||||
|
||||
initErr := state.InitFromFile(common.ConfigPath)
|
||||
if initErr != nil {
|
||||
// if error is critical, notify and return it without starting providers
|
||||
var criticalErr CriticalError
|
||||
if errors.As(initErr, &criticalErr) {
|
||||
logNotifyError("init", criticalErr.err)
|
||||
return criticalErr.err
|
||||
}
|
||||
}
|
||||
|
||||
// disable pool logging temporary since we already have pretty logging
|
||||
state.Entrypoint().DisablePoolsLog(true)
|
||||
routes.HTTP.DisableLog(true)
|
||||
routes.Stream.DisableLog(true)
|
||||
|
||||
defer func() {
|
||||
state.Entrypoint().DisablePoolsLog(false)
|
||||
routes.HTTP.DisableLog(false)
|
||||
routes.Stream.DisableLog(false)
|
||||
}()
|
||||
|
||||
initErr := state.InitFromFile(common.ConfigPath)
|
||||
err := errors.Join(initErr, state.StartProviders())
|
||||
if err != nil {
|
||||
logNotifyError("init", err)
|
||||
}
|
||||
|
||||
state.StartAPIServers()
|
||||
state.StartMetrics()
|
||||
|
||||
SetState(state)
|
||||
|
||||
// flush temporary log
|
||||
@@ -116,9 +108,7 @@ func Reload() gperr.Error {
|
||||
logNotifyError("start providers", err)
|
||||
return nil // continue
|
||||
}
|
||||
|
||||
newState.StartAPIServers()
|
||||
newState.StartMetrics()
|
||||
StartProxyServers()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -152,3 +142,16 @@ func OnConfigChange(ev []events.Event) {
|
||||
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,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"iter"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -17,20 +18,14 @@ import (
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/rs/zerolog"
|
||||
acl "github.com/yusing/godoxy/internal/acl/types"
|
||||
"github.com/yusing/godoxy/internal/acl"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
"github.com/yusing/godoxy/internal/api"
|
||||
"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"
|
||||
"github.com/yusing/godoxy/internal/entrypoint"
|
||||
entrypointctx "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
homepage "github.com/yusing/godoxy/internal/homepage/types"
|
||||
"github.com/yusing/godoxy/internal/logging"
|
||||
"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"
|
||||
route "github.com/yusing/godoxy/internal/route/provider"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
@@ -45,7 +40,7 @@ type state struct {
|
||||
|
||||
providers *xsync.Map[string, types.RouteProvider]
|
||||
autocertProvider *autocert.Provider
|
||||
entrypoint *entrypoint.Entrypoint
|
||||
entrypoint entrypoint.Entrypoint
|
||||
|
||||
task *task.Task
|
||||
|
||||
@@ -55,25 +50,14 @@ type state struct {
|
||||
tmpLog zerolog.Logger
|
||||
}
|
||||
|
||||
type CriticalError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e CriticalError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e CriticalError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func NewState() config.State {
|
||||
tmpLogBuf := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||
return &state{
|
||||
providers: xsync.NewMap[string, types.RouteProvider](),
|
||||
task: task.RootTask("config", false),
|
||||
tmpLogBuf: tmpLogBuf,
|
||||
tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf),
|
||||
providers: xsync.NewMap[string, types.RouteProvider](),
|
||||
entrypoint: entrypoint.NewEntrypoint(),
|
||||
task: task.RootTask("config", false),
|
||||
tmpLogBuf: tmpLogBuf,
|
||||
tmpLog: logging.NewLoggerWithFixedLevel(zerolog.InfoLevel, tmpLogBuf),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +73,7 @@ func SetState(state config.State) {
|
||||
|
||||
cfg := state.Value()
|
||||
config.ActiveState.Store(state)
|
||||
entrypoint.ActiveConfig.Store(&cfg.Entrypoint)
|
||||
homepage.ActiveConfig.Store(&cfg.Homepage)
|
||||
if autocertProvider := state.AutoCertProvider(); autocertProvider != nil {
|
||||
autocert.ActiveProvider.Store(autocertProvider.(*autocert.Provider))
|
||||
@@ -111,7 +96,7 @@ func (state *state) InitFromFile(filename string) error {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
state.Config = config.DefaultConfig()
|
||||
} else {
|
||||
return CriticalError{err}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return state.Init(data)
|
||||
@@ -120,7 +105,7 @@ func (state *state) InitFromFile(filename string) error {
|
||||
func (state *state) Init(data []byte) error {
|
||||
err := serialization.UnmarshalValidate(data, &state.Config, yaml.Unmarshal)
|
||||
if err != nil {
|
||||
return CriticalError{err}
|
||||
return err
|
||||
}
|
||||
|
||||
g := gperr.NewGroup("config load error")
|
||||
@@ -132,9 +117,7 @@ func (state *state) Init(data []byte) error {
|
||||
// these won't benefit from running on goroutines
|
||||
errs.Add(state.initNotification())
|
||||
errs.Add(state.initACL())
|
||||
if err := state.initEntrypoint(); err != nil {
|
||||
errs.Add(CriticalError{err})
|
||||
}
|
||||
errs.Add(state.initEntrypoint())
|
||||
errs.Add(state.loadRouteProviders())
|
||||
return errs.Error()
|
||||
}
|
||||
@@ -151,8 +134,8 @@ func (state *state) Value() *config.Config {
|
||||
return &state.Config
|
||||
}
|
||||
|
||||
func (state *state) Entrypoint() entrypointctx.Entrypoint {
|
||||
return state.entrypoint
|
||||
func (state *state) EntrypointHandler() http.Handler {
|
||||
return &state.entrypoint
|
||||
}
|
||||
|
||||
func (state *state) ShortLinkMatcher() config.ShortLinkMatcher {
|
||||
@@ -207,29 +190,6 @@ func (state *state) FlushTmpLog() {
|
||||
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.
|
||||
func (state *state) initACL() error {
|
||||
if !state.ACL.Valid() {
|
||||
@@ -239,7 +199,7 @@ func (state *state) initACL() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
acl.SetCtx(state.task, state.ACL)
|
||||
state.task.SetValue(acl.ContextKey{}, state.ACL)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -247,7 +207,6 @@ func (state *state) initEntrypoint() error {
|
||||
epCfg := state.Config.Entrypoint
|
||||
matchDomains := state.MatchDomains
|
||||
|
||||
state.entrypoint = entrypoint.NewEntrypoint(state.task, &epCfg)
|
||||
state.entrypoint.SetFindRouteDomains(matchDomains)
|
||||
state.entrypoint.SetNotFoundRules(epCfg.Rules.NotFound)
|
||||
|
||||
@@ -261,8 +220,6 @@ func (state *state) initEntrypoint() error {
|
||||
}
|
||||
}
|
||||
|
||||
entrypointctx.SetCtx(state.task, state.entrypoint)
|
||||
|
||||
errs := gperr.NewBuilder("entrypoint error")
|
||||
errs.Add(state.entrypoint.SetMiddlewares(epCfg.Middlewares))
|
||||
errs.Add(state.entrypoint.SetAccessLogger(state.task, epCfg.AccessLog))
|
||||
@@ -339,7 +296,6 @@ func (state *state) initAutoCert() error {
|
||||
p.PrintCertExpiriesAll()
|
||||
|
||||
state.autocertProvider = p
|
||||
autocertctx.SetCtx(state.task, p)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/acl"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
"github.com/yusing/godoxy/internal/entrypoint"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
homepage "github.com/yusing/godoxy/internal/homepage/types"
|
||||
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
||||
"github.com/yusing/godoxy/internal/notif"
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"iter"
|
||||
"net/http"
|
||||
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/goutils/server"
|
||||
"github.com/yusing/goutils/synk"
|
||||
@@ -22,7 +21,7 @@ type State interface {
|
||||
|
||||
Value() *Config
|
||||
|
||||
Entrypoint() entrypoint.Entrypoint
|
||||
EntrypointHandler() http.Handler
|
||||
ShortLinkMatcher() ShortLinkMatcher
|
||||
AutoCertProvider() server.CertProvider
|
||||
|
||||
@@ -33,9 +32,6 @@ type State interface {
|
||||
StartProviders() error
|
||||
|
||||
FlushTmpLog()
|
||||
|
||||
StartAPIServers()
|
||||
StartMetrics()
|
||||
}
|
||||
|
||||
type ShortLinkMatcher interface {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/yusing/godoxy/internal/dnsproviders
|
||||
|
||||
go 1.25.7
|
||||
go 1.25.6
|
||||
|
||||
replace github.com/yusing/godoxy => ../..
|
||||
|
||||
|
||||
@@ -1,129 +1,47 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"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/route/rules"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/pool"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
type HTTPRoutes interface {
|
||||
Get(alias string) (types.HTTPRoute, bool)
|
||||
}
|
||||
|
||||
type findRouteFunc func(HTTPRoutes, string) types.HTTPRoute
|
||||
|
||||
type Entrypoint struct {
|
||||
task *task.Task
|
||||
|
||||
cfg *Config
|
||||
|
||||
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
|
||||
middleware *middleware.Middleware
|
||||
notFoundHandler http.Handler
|
||||
accessLogger accesslog.AccessLogger
|
||||
findRouteFunc func(host string) types.HTTPRoute
|
||||
shortLinkTree *ShortLinkMatcher
|
||||
}
|
||||
|
||||
var _ entrypoint.Entrypoint = &Entrypoint{}
|
||||
// nil-safe
|
||||
var ActiveConfig atomic.Pointer[entrypoint.Config]
|
||||
|
||||
var emptyCfg Config
|
||||
|
||||
func NewEntrypoint(parent task.Parent, cfg *Config) *Entrypoint {
|
||||
if cfg == nil {
|
||||
cfg = &emptyCfg
|
||||
}
|
||||
|
||||
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 init() {
|
||||
// make sure it's not nil
|
||||
ActiveConfig.Store(&entrypoint.Config{})
|
||||
}
|
||||
|
||||
func (ep *Entrypoint) SupportProxyProtocol() bool {
|
||||
return ep.cfg.SupportProxyProtocol
|
||||
}
|
||||
|
||||
func (ep *Entrypoint) DisablePoolsLog(v bool) {
|
||||
ep.httpPoolDisableLog.Store(v)
|
||||
// apply to all running http servers
|
||||
for _, srv := range ep.servers.Range {
|
||||
srv.routes.DisableLog(v)
|
||||
func NewEntrypoint() Entrypoint {
|
||||
return Entrypoint{
|
||||
findRouteFunc: findRouteAnyDomain,
|
||||
shortLinkTree: newShortLinkTree(),
|
||||
}
|
||||
// apply to other pools
|
||||
ep.streamRoutes.DisableLog(v)
|
||||
ep.excludedRoutes.DisableLog(v)
|
||||
}
|
||||
|
||||
func (ep *Entrypoint) ShortLinkMatcher() *ShortLinkMatcher {
|
||||
return ep.shortLinkMatcher
|
||||
}
|
||||
|
||||
func (ep *Entrypoint) HTTPRoutes() entrypoint.PoolLike[types.HTTPRoute] {
|
||||
return newHTTPPoolAdapter(ep)
|
||||
}
|
||||
|
||||
func (ep *Entrypoint) StreamRoutes() entrypoint.PoolLike[types.StreamRoute] {
|
||||
return ep.streamRoutes
|
||||
}
|
||||
|
||||
func (ep *Entrypoint) ExcludedRoutes() entrypoint.RWPoolLike[types.Route] {
|
||||
return ep.excludedRoutes
|
||||
}
|
||||
|
||||
func (ep *Entrypoint) GetServer(addr string) (http.Handler, bool) {
|
||||
return ep.servers.Load(addr)
|
||||
return ep.shortLinkTree
|
||||
}
|
||||
|
||||
func (ep *Entrypoint) SetFindRouteDomains(domains []string) {
|
||||
@@ -156,7 +74,7 @@ func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error {
|
||||
}
|
||||
|
||||
func (ep *Entrypoint) SetNotFoundRules(rules rules.Rules) {
|
||||
ep.notFoundHandler = rules.BuildHandler(serveNotFound)
|
||||
ep.notFoundHandler = rules.BuildHandler(http.HandlerFunc(ep.serveNotFound))
|
||||
}
|
||||
|
||||
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) (err error) {
|
||||
@@ -173,39 +91,111 @@ func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.Request
|
||||
return err
|
||||
}
|
||||
|
||||
func findRouteAnyDomain(routes HTTPRoutes, host string) types.HTTPRoute {
|
||||
func (ep *Entrypoint) FindRoute(s 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.shortLinkTree.ServeHTTP, w, r)
|
||||
} else {
|
||||
ep.shortLinkTree.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, '.')
|
||||
if idx != -1 {
|
||||
target := host[:idx]
|
||||
if r, ok := routes.Get(target); ok {
|
||||
if r, ok := routes.HTTP.Get(target); ok {
|
||||
return r
|
||||
}
|
||||
}
|
||||
if r, ok := routes.Get(host); ok {
|
||||
if r, ok := routes.HTTP.Get(host); ok {
|
||||
return r
|
||||
}
|
||||
// try striping the trailing :port from the host
|
||||
if before, _, ok := strings.Cut(host, ":"); ok {
|
||||
if r, ok := routes.Get(before); ok {
|
||||
if r, ok := routes.HTTP.Get(before); ok {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findRouteByDomains(domains []string) func(routes HTTPRoutes, host string) types.HTTPRoute {
|
||||
return func(routes HTTPRoutes, host string) types.HTTPRoute {
|
||||
func findRouteByDomains(domains []string) func(host string) types.HTTPRoute {
|
||||
return func(host string) types.HTTPRoute {
|
||||
host, _, _ = strings.Cut(host, ":") // strip the trailing :port
|
||||
for _, domain := range domains {
|
||||
if target, ok := strings.CutSuffix(host, domain); ok {
|
||||
if r, ok := routes.Get(target); ok {
|
||||
if r, ok := routes.HTTP.Get(target); ok {
|
||||
return r
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to exact match
|
||||
if r, ok := routes.Get(host); ok {
|
||||
if r, ok := routes.HTTP.Get(host); ok {
|
||||
return r
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -10,11 +10,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
. "github.com/yusing/godoxy/internal/entrypoint"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/route"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
routeTypes "github.com/yusing/godoxy/internal/route/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/goutils/task"
|
||||
@@ -50,15 +48,13 @@ func (t noopTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
}
|
||||
|
||||
func BenchmarkEntrypointReal(b *testing.B) {
|
||||
task := task.NewTestTask(b)
|
||||
ep := NewEntrypoint(task, nil)
|
||||
var ep Entrypoint
|
||||
req := http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/", RawPath: "/"},
|
||||
Host: "test.domain.tld",
|
||||
}
|
||||
ep.SetFindRouteDomains([]string{})
|
||||
entrypoint.SetCtx(task, ep)
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Length", "1")
|
||||
@@ -81,48 +77,48 @@ func BenchmarkEntrypointReal(b *testing.B) {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
r, err := route.NewTestRoute(b, task, &route.Route{
|
||||
r := &route.Route{
|
||||
Alias: "test",
|
||||
Scheme: routeTypes.SchemeHTTP,
|
||||
Host: host,
|
||||
Port: route.Port{Proxy: portInt},
|
||||
HealthCheck: types.HealthCheckConfig{Disable: true},
|
||||
})
|
||||
}
|
||||
|
||||
require.NoError(b, err)
|
||||
require.False(b, r.ShouldExclude())
|
||||
err = r.Validate()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
err = r.Start(task.RootTask("test", false))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
var w noopResponseWriter
|
||||
|
||||
server, ok := ep.GetServer(common.ProxyHTTPAddr)
|
||||
if !ok {
|
||||
b.Fatal("server not found")
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
server.ServeHTTP(&w, &req)
|
||||
if w.statusCode != http.StatusOK {
|
||||
b.Fatalf("status code is not 200: %d", w.statusCode)
|
||||
}
|
||||
if string(w.written) != "1" {
|
||||
b.Fatalf("written is not 1: %s", string(w.written))
|
||||
}
|
||||
ep.ServeHTTP(&w, &req)
|
||||
// if w.statusCode != http.StatusOK {
|
||||
// b.Fatalf("status code is not 200: %d", w.statusCode)
|
||||
// }
|
||||
// if string(w.written) != "1" {
|
||||
// b.Fatalf("written is not 1: %s", string(w.written))
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEntrypoint(b *testing.B) {
|
||||
task := task.NewTestTask(b)
|
||||
ep := NewEntrypoint(task, nil)
|
||||
var ep Entrypoint
|
||||
req := http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/", RawPath: "/"},
|
||||
Host: "test.domain.tld",
|
||||
}
|
||||
ep.SetFindRouteDomains([]string{})
|
||||
entrypoint.SetCtx(task, ep)
|
||||
|
||||
r, err := route.NewTestRoute(b, task, &route.Route{
|
||||
r := &route.Route{
|
||||
Alias: "test",
|
||||
Scheme: routeTypes.SchemeHTTP,
|
||||
Host: "localhost",
|
||||
@@ -132,23 +128,29 @@ func BenchmarkEntrypoint(b *testing.B) {
|
||||
HealthCheck: types.HealthCheckConfig{
|
||||
Disable: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
require.NoError(b, err)
|
||||
require.False(b, r.ShouldExclude())
|
||||
err := r.Validate()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
r.(types.ReverseProxyRoute).ReverseProxy().Transport = noopTransport{}
|
||||
err = r.Start(task.RootTask("test", false))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
rev, ok := routes.HTTP.Get("test")
|
||||
if !ok {
|
||||
b.Fatal("route not found")
|
||||
}
|
||||
rev.(types.ReverseProxyRoute).ReverseProxy().Transport = noopTransport{}
|
||||
|
||||
var w noopResponseWriter
|
||||
|
||||
server, ok := ep.GetServer(common.ProxyHTTPAddr)
|
||||
if !ok {
|
||||
b.Fatal("server not found")
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
server.ServeHTTP(&w, &req)
|
||||
ep.ServeHTTP(&w, &req)
|
||||
if w.statusCode != http.StatusOK {
|
||||
b.Fatalf("status code is not 200: %d", w.statusCode)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@ import (
|
||||
|
||||
. "github.com/yusing/godoxy/internal/entrypoint"
|
||||
"github.com/yusing/godoxy/internal/route"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
|
||||
"github.com/yusing/goutils/task"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
func addRoute(ep *Entrypoint, alias string) {
|
||||
ep.AddRoute(&route.ReveseProxyRoute{
|
||||
var ep = NewEntrypoint()
|
||||
|
||||
func addRoute(alias string) {
|
||||
routes.HTTP.Add(&route.ReveseProxyRoute{
|
||||
Route: &route.Route{
|
||||
Alias: alias,
|
||||
Port: route.Port{
|
||||
@@ -23,28 +25,26 @@ func addRoute(ep *Entrypoint, alias string) {
|
||||
|
||||
func run(t *testing.T, match []string, noMatch []string) {
|
||||
t.Helper()
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
t.Cleanup(routes.Clear)
|
||||
t.Cleanup(func() { ep.SetFindRouteDomains(nil) })
|
||||
|
||||
for _, test := range match {
|
||||
t.Run(test, func(t *testing.T) {
|
||||
found, ok := ep.HTTPRoutes().Get(test)
|
||||
expect.True(t, ok)
|
||||
found := ep.FindRoute(test)
|
||||
expect.NotNil(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
for _, test := range noMatch {
|
||||
t.Run(test, func(t *testing.T) {
|
||||
found, ok := ep.HTTPRoutes().Get(test)
|
||||
expect.False(t, ok)
|
||||
found := ep.FindRoute(test)
|
||||
expect.Nil(t, found)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRouteAnyDomain(t *testing.T) {
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
addRoute(ep, "app1")
|
||||
addRoute("app1")
|
||||
|
||||
tests := []string{
|
||||
"app1.com",
|
||||
@@ -62,7 +62,6 @@ func TestFindRouteAnyDomain(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFindRouteExactHostMatch(t *testing.T) {
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
tests := []string{
|
||||
"app2.com",
|
||||
"app2.domain.com",
|
||||
@@ -76,20 +75,19 @@ func TestFindRouteExactHostMatch(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
addRoute(ep, test)
|
||||
addRoute(test)
|
||||
}
|
||||
|
||||
run(t, tests, testsNoMatch)
|
||||
}
|
||||
|
||||
func TestFindRouteByDomains(t *testing.T) {
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
ep.SetFindRouteDomains([]string{
|
||||
".domain.com",
|
||||
".sub.domain.com",
|
||||
})
|
||||
|
||||
addRoute(ep, "app1")
|
||||
addRoute("app1")
|
||||
|
||||
tests := []string{
|
||||
"app1.domain.com",
|
||||
@@ -109,13 +107,12 @@ func TestFindRouteByDomains(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFindRouteByDomainsExactMatch(t *testing.T) {
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
ep.SetFindRouteDomains([]string{
|
||||
".domain.com",
|
||||
".sub.domain.com",
|
||||
})
|
||||
|
||||
addRoute(ep, "app1.foo.bar")
|
||||
addRoute("app1.foo.bar")
|
||||
|
||||
tests := []string{
|
||||
"app1.foo.bar", // exact match
|
||||
@@ -134,9 +131,8 @@ func TestFindRouteByDomainsExactMatch(t *testing.T) {
|
||||
|
||||
func TestFindRouteWithPort(t *testing.T) {
|
||||
t.Run("AnyDomain", func(t *testing.T) {
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
addRoute(ep, "app1")
|
||||
addRoute(ep, "app2.com")
|
||||
addRoute("app1")
|
||||
addRoute("app2.com")
|
||||
|
||||
tests := []string{
|
||||
"app1:8080",
|
||||
@@ -152,13 +148,12 @@ func TestFindRouteWithPort(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ByDomains", func(t *testing.T) {
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
ep.SetFindRouteDomains([]string{
|
||||
".domain.com",
|
||||
})
|
||||
addRoute(ep, "app1")
|
||||
addRoute(ep, "app2")
|
||||
addRoute(ep, "app3.domain.com")
|
||||
addRoute("app1")
|
||||
addRoute("app2")
|
||||
addRoute("app3.domain.com")
|
||||
|
||||
tests := []string{
|
||||
"app1.domain.com:8080",
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"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:
|
||||
if err := ep.AddHTTPRoute(r); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("route", r.Key()).
|
||||
Str("listen_url", r.ListenURL().String()).
|
||||
Msg("failed to add HTTP route")
|
||||
}
|
||||
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()
|
||||
var httpAddr, httpsAddr string
|
||||
if host == "" {
|
||||
httpAddr = common.ProxyHTTPAddr
|
||||
httpsAddr = common.ProxyHTTPSAddr
|
||||
} else {
|
||||
httpAddr = net.JoinHostPort(host, strconv.Itoa(common.ProxyHTTPPort))
|
||||
httpsAddr = net.JoinHostPort(host, strconv.Itoa(common.ProxyHTTPSPort))
|
||||
}
|
||||
return 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() (newSrv *httpServer, cancel bool) {
|
||||
newSrv = newHTTPServer(ep)
|
||||
err = newSrv.Listen(addr, proto)
|
||||
cancel = err != nil
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srv.AddRoute(route)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *Entrypoint) delHTTPRoute(route types.HTTPRoute) {
|
||||
addr := route.ListenURL().Host
|
||||
srv, _ := ep.servers.Load(addr)
|
||||
if srv != nil {
|
||||
srv.DelRoute(route)
|
||||
}
|
||||
// TODO: close if no servers left
|
||||
}
|
||||
@@ -14,7 +14,7 @@ type ShortLinkMatcher struct {
|
||||
subdomainRoutes *xsync.Map[string, struct{}]
|
||||
}
|
||||
|
||||
func newShortLinkMatcher() *ShortLinkMatcher {
|
||||
func newShortLinkTree() *ShortLinkMatcher {
|
||||
return &ShortLinkMatcher{
|
||||
fqdnRoutes: xsync.NewMap[string, string](),
|
||||
subdomainRoutes: xsync.NewMap[string, struct{}](),
|
||||
|
||||
@@ -6,15 +6,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
. "github.com/yusing/godoxy/internal/entrypoint"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
func TestShortLinkMatcher_FQDNAlias(t *testing.T) {
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
ep := NewEntrypoint()
|
||||
matcher := ep.ShortLinkMatcher()
|
||||
matcher.AddRoute("app.domain.com")
|
||||
|
||||
@@ -47,7 +45,7 @@ func TestShortLinkMatcher_FQDNAlias(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShortLinkMatcher_SubdomainAlias(t *testing.T) {
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
ep := NewEntrypoint()
|
||||
matcher := ep.ShortLinkMatcher()
|
||||
matcher.SetDefaultDomainSuffix(".example.com")
|
||||
matcher.AddRoute("app")
|
||||
@@ -72,7 +70,7 @@ func TestShortLinkMatcher_SubdomainAlias(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShortLinkMatcher_NotFound(t *testing.T) {
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
ep := NewEntrypoint()
|
||||
matcher := ep.ShortLinkMatcher()
|
||||
matcher.SetDefaultDomainSuffix(".example.com")
|
||||
matcher.AddRoute("app")
|
||||
@@ -95,7 +93,7 @@ func TestShortLinkMatcher_NotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShortLinkMatcher_AddDelRoute(t *testing.T) {
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
ep := NewEntrypoint()
|
||||
matcher := ep.ShortLinkMatcher()
|
||||
matcher.SetDefaultDomainSuffix(".example.com")
|
||||
|
||||
@@ -133,7 +131,7 @@ func TestShortLinkMatcher_AddDelRoute(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShortLinkMatcher_NoDefaultDomainSuffix(t *testing.T) {
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
ep := NewEntrypoint()
|
||||
matcher := ep.ShortLinkMatcher()
|
||||
// no SetDefaultDomainSuffix called
|
||||
|
||||
@@ -160,19 +158,15 @@ func TestShortLinkMatcher_NoDefaultDomainSuffix(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEntrypoint_ShortLinkDispatch(t *testing.T) {
|
||||
ep := NewEntrypoint(task.NewTestTask(t), nil)
|
||||
ep := NewEntrypoint()
|
||||
ep.ShortLinkMatcher().SetDefaultDomainSuffix(".example.com")
|
||||
ep.ShortLinkMatcher().AddRoute("app")
|
||||
|
||||
server := NewHTTPServer(ep)
|
||||
err := server.Listen("localhost:0", HTTPProtoHTTP)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("shortlink host", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/app", nil)
|
||||
req.Host = common.ShortLinkPrefix
|
||||
w := httptest.NewRecorder()
|
||||
server.ServeHTTP(w, req)
|
||||
ep.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
|
||||
assert.Equal(t, "https://app.example.com/", w.Header().Get("Location"))
|
||||
@@ -182,7 +176,7 @@ func TestEntrypoint_ShortLinkDispatch(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/app", nil)
|
||||
req.Host = common.ShortLinkPrefix + ":8080"
|
||||
w := httptest.NewRecorder()
|
||||
server.ServeHTTP(w, req)
|
||||
ep.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
|
||||
assert.Equal(t, "https://app.example.com/", w.Header().Get("Location"))
|
||||
@@ -192,7 +186,7 @@ func TestEntrypoint_ShortLinkDispatch(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/app", nil)
|
||||
req.Host = "app.example.com"
|
||||
w := httptest.NewRecorder()
|
||||
server.ServeHTTP(w, req)
|
||||
ep.ServeHTTP(w, req)
|
||||
|
||||
// Should not redirect, should try normal route lookup (which will 404)
|
||||
assert.NotEqual(t, http.StatusTemporaryRedirect, w.Code)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
)
|
||||
|
||||
type Entrypoint interface {
|
||||
SupportProxyProtocol() bool
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -12,14 +12,6 @@ import (
|
||||
)
|
||||
|
||||
func Stream(ctx context.Context, url *url.URL, timeout time.Duration) (types.HealthCheckResult, error) {
|
||||
if port := url.Port(); port == "" || port == "0" {
|
||||
return types.HealthCheckResult{
|
||||
Latency: 0,
|
||||
Healthy: false,
|
||||
Detail: "no port specified",
|
||||
}, nil
|
||||
}
|
||||
|
||||
dialer := net.Dialer{
|
||||
Timeout: timeout,
|
||||
FallbackDelay: -1,
|
||||
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
"github.com/yusing/ds/ordered"
|
||||
config "github.com/yusing/godoxy/internal/config/types"
|
||||
"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/idlewatcher/provider"
|
||||
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/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/watcher/events"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
@@ -173,7 +173,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
depRoute, ok = entrypoint.FromCtx(parent.Context()).GetRoute(dep)
|
||||
depRoute, ok = routes.GetIncludeExcluded(dep)
|
||||
if !ok {
|
||||
depErrors.Addf("dependency %q not found", dep)
|
||||
continue
|
||||
|
||||
@@ -153,8 +153,8 @@ func (p *Poller[T, AggregateT]) pollWithTimeout(ctx context.Context) {
|
||||
p.lastResult.Store(data)
|
||||
}
|
||||
|
||||
func (p *Poller[T, AggregateT]) Start(parent task.Parent) {
|
||||
t := parent.Subtask("poller."+p.name, true)
|
||||
func (p *Poller[T, AggregateT]) Start() {
|
||||
t := task.RootTask("poller."+p.name, true)
|
||||
l := log.With().Str("name", p.name).Logger()
|
||||
err := p.load()
|
||||
if err != nil {
|
||||
|
||||
@@ -8,17 +8,16 @@ import (
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"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"
|
||||
metricsutils "github.com/yusing/godoxy/internal/metrics/utils"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
)
|
||||
|
||||
type (
|
||||
StatusByAlias struct {
|
||||
Map map[string]types.HealthInfoWithoutDetail `json:"statuses"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Map map[string]routes.HealthInfoWithoutDetail `json:"statuses"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
} // @name RouteStatusesByAlias
|
||||
Status struct {
|
||||
Status types.HealthStatus `json:"status" swaggertype:"string" enums:"healthy,unhealthy,unknown,napping,starting"`
|
||||
@@ -42,7 +41,7 @@ var Poller = period.NewPoller("uptime", getStatuses, aggregateStatuses)
|
||||
|
||||
func getStatuses(ctx context.Context, _ StatusByAlias) (StatusByAlias, error) {
|
||||
return StatusByAlias{
|
||||
Map: entrypoint.FromCtx(ctx).GetHealthInfoWithoutDetail(),
|
||||
Map: routes.GetHealthInfoWithoutDetail(),
|
||||
Timestamp: time.Now().Unix(),
|
||||
}, nil
|
||||
}
|
||||
@@ -128,13 +127,11 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
|
||||
up, down, idle, latency := rs.calculateInfo(statuses)
|
||||
|
||||
status := types.StatusUnknown
|
||||
if state := config.ActiveState.Load(); state != nil {
|
||||
r, ok := entrypoint.FromCtx(state.Context()).GetRoute(alias)
|
||||
if ok {
|
||||
mon := r.HealthMonitor()
|
||||
if mon != nil {
|
||||
status = mon.Status()
|
||||
}
|
||||
r, ok := routes.GetIncludeExcluded(alias)
|
||||
if ok {
|
||||
mon := r.HealthMonitor()
|
||||
if mon != nil {
|
||||
status = mon.Status()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ func TestEntrypointBypassRoute(t *testing.T) {
|
||||
expect.NoError(t, err)
|
||||
|
||||
expect.NoError(t, err)
|
||||
entry := entrypoint.NewEntrypoint(task.NewTestTask(t), nil)
|
||||
entry := entrypoint.NewEntrypoint()
|
||||
r := &route.Route{
|
||||
Alias: "test-route",
|
||||
Host: host,
|
||||
@@ -260,11 +260,7 @@ func TestEntrypointBypassRoute(t *testing.T) {
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "http://test-route.example.com", nil)
|
||||
server, ok := entry.GetServer(r.ListenURL().Host)
|
||||
if !ok {
|
||||
t.Fatal("server not found")
|
||||
}
|
||||
server.ServeHTTP(recorder, req)
|
||||
entry.ServeHTTP(recorder, req)
|
||||
expect.Equal(t, recorder.Code, http.StatusOK, "should bypass http redirect")
|
||||
expect.Equal(t, recorder.Body.String(), "test")
|
||||
expect.Equal(t, recorder.Header().Get("Test-Header"), "test-value")
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
ioutils "github.com/yusing/goutils/io"
|
||||
)
|
||||
@@ -66,7 +66,7 @@ func (m *crowdsecMiddleware) finalize() error {
|
||||
// before implements RequestModifier.
|
||||
func (m *crowdsecMiddleware) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
// Build CrowdSec URL
|
||||
crowdsecURL, err := m.buildCrowdSecURL(r.Context())
|
||||
crowdsecURL, err := m.buildCrowdSecURL()
|
||||
if err != nil {
|
||||
Crowdsec.LogError(r).Err(err).Msg("failed to build CrowdSec URL")
|
||||
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
|
||||
func (m *crowdsecMiddleware) buildCrowdSecURL(ctx context.Context) (string, error) {
|
||||
func (m *crowdsecMiddleware) buildCrowdSecURL() (string, error) {
|
||||
// Try to get route first
|
||||
if m.Route != "" {
|
||||
if route, ok := entrypoint.FromCtx(ctx).GetRoute(m.Route); ok {
|
||||
if route, ok := routes.HTTP.Get(m.Route); ok {
|
||||
// Using route name
|
||||
targetURL := *route.TargetURL()
|
||||
targetURL.Path = m.Endpoint
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
)
|
||||
@@ -46,7 +46,7 @@ func (m *forwardAuthMiddleware) setup() {
|
||||
|
||||
// before implements RequestModifier.
|
||||
func (m *forwardAuthMiddleware) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
route, ok := entrypoint.FromCtx(r.Context()).HTTPRoutes().Get(m.Route)
|
||||
route, ok := routes.HTTP.Get(m.Route)
|
||||
if !ok {
|
||||
ForwardAuth.LogWarn(r).Str("route", m.Route).Msg("forwardauth route not found")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
func checkExists(r types.Route) gperr.Error {
|
||||
if r.UseLoadBalance() { // skip checking for load balanced routes
|
||||
return nil
|
||||
}
|
||||
@@ -21,9 +16,9 @@ func checkExists(ctx context.Context, r types.Route) gperr.Error {
|
||||
)
|
||||
switch r := r.(type) {
|
||||
case types.HTTPRoute:
|
||||
existing, ok = entrypoint.FromCtx(ctx).HTTPRoutes().Get(r.Key())
|
||||
existing, ok = routes.HTTP.Get(r.Key())
|
||||
case types.StreamRoute:
|
||||
existing, ok = entrypoint.FromCtx(ctx).StreamRoutes().Get(r.Key())
|
||||
existing, ok = routes.Stream.Get(r.Key())
|
||||
}
|
||||
if ok {
|
||||
return gperr.Errorf("route already exists: from provider %s and %s", existing.ProviderName(), r.ProviderName())
|
||||
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
config "github.com/yusing/godoxy/internal/config/types"
|
||||
"github.com/yusing/godoxy/internal/health/monitor"
|
||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/task"
|
||||
@@ -120,19 +120,20 @@ func (s *FileServer) Start(parent task.Parent) gperr.Error {
|
||||
if s.UseHealthCheck() {
|
||||
s.HealthMon = monitor.NewMonitor(s)
|
||||
if err := s.HealthMon.Start(s.task); err != nil {
|
||||
l := log.With().Str("type", "fileserver").Str("name", s.Name()).Logger()
|
||||
gperr.LogWarn("health monitor error", err, &l)
|
||||
s.HealthMon = nil
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ep := entrypoint.FromCtx(parent.Context())
|
||||
if ep == nil {
|
||||
err := gperr.New("entrypoint not initialized")
|
||||
s.task.Finish(err)
|
||||
return err
|
||||
routes.HTTP.Add(s)
|
||||
if state := config.WorkingState.Load(); state != nil {
|
||||
state.ShortLinkMatcher().AddRoute(s.Alias)
|
||||
}
|
||||
ep.AddRoute(s)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/agentproxy"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
config "github.com/yusing/godoxy/internal/config/types"
|
||||
"github.com/yusing/godoxy/internal/health/monitor"
|
||||
"github.com/yusing/godoxy/internal/idlewatcher"
|
||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/loadbalancer"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
route "github.com/yusing/godoxy/internal/route/types"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
@@ -158,22 +159,23 @@ func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error {
|
||||
|
||||
if r.HealthMon != nil {
|
||||
if err := r.HealthMon.Start(r.task); err != nil {
|
||||
gperr.LogWarn("health monitor error", err, &r.rp.Logger)
|
||||
r.HealthMon = nil
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ep := entrypoint.FromCtx(parent.Context())
|
||||
if ep == nil {
|
||||
err := gperr.New("entrypoint not initialized")
|
||||
r.task.Finish(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if r.UseLoadBalance() {
|
||||
r.addToLoadBalancer(parent, ep)
|
||||
r.addToLoadBalancer(parent)
|
||||
} else {
|
||||
ep.AddRoute(r)
|
||||
routes.HTTP.Add(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
|
||||
}
|
||||
@@ -185,12 +187,12 @@ func (r *ReveseProxyRoute) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
var lbLock sync.Mutex
|
||||
|
||||
func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent, ep entrypoint.Entrypoint) {
|
||||
func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
|
||||
var lb *loadbalancer.LoadBalancer
|
||||
cfg := r.LoadBalance
|
||||
lbLock.Lock()
|
||||
|
||||
l, ok := ep.HTTPRoutes().Get(cfg.Link)
|
||||
l, ok := routes.HTTP.Get(cfg.Link)
|
||||
var linked *ReveseProxyRoute
|
||||
if ok {
|
||||
lbLock.Unlock()
|
||||
@@ -212,7 +214,16 @@ func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent, ep entrypoint.E
|
||||
handler: lb,
|
||||
}
|
||||
linked.SetHealthMonitor(lb)
|
||||
ep.AddRoute(linked)
|
||||
routes.HTTP.AddKey(cfg.Link, 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()
|
||||
}
|
||||
r.loadBalancer = lb
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
config "github.com/yusing/godoxy/internal/config/types"
|
||||
"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/homepage"
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
@@ -34,6 +33,7 @@ import (
|
||||
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/godoxy/internal/route/rules"
|
||||
rulepresets "github.com/yusing/godoxy/internal/route/rules/presets"
|
||||
route "github.com/yusing/godoxy/internal/route/types"
|
||||
@@ -46,6 +46,7 @@ type (
|
||||
Host string `json:"host,omitempty"`
|
||||
Port route.Port `json:"port"`
|
||||
|
||||
// for TCP and UDP routes, bind address to listen on
|
||||
Bind string `json:"bind,omitempty" validate:"omitempty,ip_addr" extensions:"x-nullable"`
|
||||
|
||||
Root string `json:"root,omitempty"`
|
||||
@@ -199,11 +200,7 @@ func (r *Route) validate() gperr.Error {
|
||||
|
||||
if (r.Proxmox == nil || r.Proxmox.Node == "" || r.Proxmox.VMID == nil) && r.Container == nil {
|
||||
wasNotNil := r.Proxmox != nil
|
||||
workingState := config.WorkingState.Load()
|
||||
var proxmoxProviders []*proxmox.Config
|
||||
if workingState != nil { // nil in tests
|
||||
proxmoxProviders = workingState.Value().Providers.Proxmox
|
||||
}
|
||||
proxmoxProviders := config.WorkingState.Load().Value().Providers.Proxmox
|
||||
if len(proxmoxProviders) > 0 {
|
||||
// it's fine if ip is nil
|
||||
hostname := r.Host
|
||||
@@ -257,7 +254,7 @@ func (r *Route) validate() gperr.Error {
|
||||
}
|
||||
|
||||
// return error if route is localhost:<godoxy_port> but route is not agent
|
||||
if !r.IsAgent() && !r.ShouldExclude() {
|
||||
if !r.IsAgent() {
|
||||
switch r.Host {
|
||||
case "localhost", "127.0.0.1":
|
||||
switch r.Port.Proxy {
|
||||
@@ -277,19 +274,24 @@ func (r *Route) validate() gperr.Error {
|
||||
var impl types.Route
|
||||
var err gperr.Error
|
||||
|
||||
if r.ShouldExclude() {
|
||||
switch r.Scheme {
|
||||
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))))
|
||||
} else {
|
||||
switch r.Scheme {
|
||||
case route.SchemeFileServer:
|
||||
r.Host = ""
|
||||
r.Port.Proxy = 0
|
||||
r.LisURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("https://%s", net.JoinHostPort(r.Bind, strconv.Itoa(r.Port.Listening))))
|
||||
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, "file://"+r.Root)
|
||||
case route.SchemeHTTP, route.SchemeHTTPS, route.SchemeH2C:
|
||||
r.LisURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("https://%s", net.JoinHostPort(r.Bind, strconv.Itoa(r.Port.Listening))))
|
||||
case route.SchemeTCP, route.SchemeUDP:
|
||||
if r.ShouldExclude() {
|
||||
// should exclude, we don't care the scheme here.
|
||||
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.Bind == "" {
|
||||
r.Bind = "0.0.0.0"
|
||||
}
|
||||
bindIP := net.ParseIP(r.Bind)
|
||||
remoteIP := net.ParseIP(r.Host)
|
||||
toNetwork := func(ip net.IP, scheme route.Scheme) string {
|
||||
@@ -358,8 +360,8 @@ func (r *Route) validateRules() error {
|
||||
return errors.New("rule preset `webui.yml` not found")
|
||||
}
|
||||
r.Rules = rules
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.RuleFile != "" && len(r.Rules) > 0 {
|
||||
@@ -502,7 +504,7 @@ func (r *Route) start(parent task.Parent) gperr.Error {
|
||||
// skip checking for excluded routes
|
||||
excluded := r.ShouldExclude()
|
||||
if !excluded {
|
||||
if err := checkExists(parent.Context(), r); err != nil {
|
||||
if err := checkExists(r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -516,22 +518,15 @@ func (r *Route) start(parent task.Parent) gperr.Error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
ep := entrypoint.FromCtx(parent.Context())
|
||||
if ep == nil {
|
||||
return gperr.New("entrypoint not initialized")
|
||||
}
|
||||
|
||||
r.task = parent.Subtask("excluded."+r.Name(), false)
|
||||
ep.ExcludedRoutes().Add(r.impl)
|
||||
r.task = parent.Subtask("excluded."+r.Name(), true)
|
||||
routes.Excluded.Add(r.impl)
|
||||
r.task.OnCancel("remove_route_from_excluded", func() {
|
||||
ep.ExcludedRoutes().Del(r.impl)
|
||||
routes.Excluded.Del(r.impl)
|
||||
})
|
||||
if r.UseHealthCheck() {
|
||||
r.HealthMon = monitor.NewMonitor(r.impl)
|
||||
err := r.HealthMon.Start(r.task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -569,10 +564,6 @@ func (r *Route) ProviderName() string {
|
||||
return r.Provider
|
||||
}
|
||||
|
||||
func (r *Route) ListenURL() *nettypes.URL {
|
||||
return r.LisURL
|
||||
}
|
||||
|
||||
func (r *Route) TargetURL() *nettypes.URL {
|
||||
return r.ProxyURL
|
||||
}
|
||||
@@ -758,7 +749,6 @@ const (
|
||||
ExcludedReasonNoPortSpecified
|
||||
ExcludedReasonBlacklisted
|
||||
ExcludedReasonBuildx
|
||||
ExcludedReasonYAMLAnchor
|
||||
ExcludedReasonOld
|
||||
)
|
||||
|
||||
@@ -778,8 +768,6 @@ func (re ExcludedReason) String() string {
|
||||
return "Blacklisted (backend service or database)"
|
||||
case ExcludedReasonBuildx:
|
||||
return "Buildx"
|
||||
case ExcludedReasonYAMLAnchor:
|
||||
return "YAML anchor or reference"
|
||||
case ExcludedReasonOld:
|
||||
return "Container renaming intermediate state"
|
||||
default:
|
||||
@@ -814,12 +802,6 @@ func (r *Route) findExcludedReason() ExcludedReason {
|
||||
} else if r.IsZeroPort() && r.Scheme != route.SchemeFileServer {
|
||||
return ExcludedReasonNoPortSpecified
|
||||
}
|
||||
// this should happen on validation API only,
|
||||
// those routes are removed before validation.
|
||||
// see removeXPrefix in provider/file.go
|
||||
if strings.HasPrefix(r.Alias, "x-") { // for YAML anchors and references
|
||||
return ExcludedReasonYAMLAnchor
|
||||
}
|
||||
if strings.HasSuffix(r.Alias, "-old") {
|
||||
return ExcludedReasonOld
|
||||
}
|
||||
@@ -941,13 +923,6 @@ func (r *Route) Finalize() {
|
||||
}
|
||||
}
|
||||
|
||||
switch r.Scheme {
|
||||
case route.SchemeTCP, route.SchemeUDP:
|
||||
if r.Bind == "" {
|
||||
r.Bind = "0.0.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
r.Port.Listening, r.Port.Proxy = lp, pp
|
||||
|
||||
workingState := config.WorkingState.Load()
|
||||
@@ -958,7 +933,6 @@ func (r *Route) Finalize() {
|
||||
panic("bug: working state is nil")
|
||||
}
|
||||
|
||||
// TODO: default value from context
|
||||
r.HealthCheck.ApplyDefaults(config.WorkingState.Load().Value().Defaults.HealthCheck)
|
||||
}
|
||||
|
||||
|
||||
307
internal/route/routes/README.md
Normal file
307
internal/route/routes/README.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# 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
|
||||
103
internal/route/routes/query.go
Normal file
103
internal/route/routes/query.go
Normal file
@@ -0,0 +1,103 @@
|
||||
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
|
||||
}
|
||||
91
internal/route/routes/routes.go
Normal file
91
internal/route/routes/routes.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/goutils/pool"
|
||||
)
|
||||
|
||||
var (
|
||||
HTTP = pool.New[types.HTTPRoute]("http_routes")
|
||||
Stream = pool.New[types.StreamRoute]("stream_routes")
|
||||
|
||||
Excluded = pool.New[types.Route]("excluded_routes")
|
||||
)
|
||||
|
||||
func IterActive(yield func(r types.Route) bool) {
|
||||
for _, r := range HTTP.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, r := range Stream.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func IterAll(yield func(r types.Route) bool) {
|
||||
for _, r := range HTTP.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, r := range Stream.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, r := range Excluded.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NumActiveRoutes() int {
|
||||
return HTTP.Size() + Stream.Size()
|
||||
}
|
||||
|
||||
func NumAllRoutes() int {
|
||||
return HTTP.Size() + Stream.Size() + Excluded.Size()
|
||||
}
|
||||
|
||||
func Clear() {
|
||||
HTTP.Clear()
|
||||
Stream.Clear()
|
||||
Excluded.Clear()
|
||||
}
|
||||
|
||||
func GetHTTPRouteOrExact(alias, host string) (types.HTTPRoute, bool) {
|
||||
r, ok := HTTP.Get(alias)
|
||||
if ok {
|
||||
return r, true
|
||||
}
|
||||
// try find with exact match
|
||||
return HTTP.Get(host)
|
||||
}
|
||||
|
||||
// Get returns the route with the given alias.
|
||||
//
|
||||
// It does not return excluded routes.
|
||||
func Get(alias string) (types.Route, bool) {
|
||||
if r, ok := HTTP.Get(alias); ok {
|
||||
return r, true
|
||||
}
|
||||
if r, ok := Stream.Get(alias); ok {
|
||||
return r, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// GetIncludeExcluded returns the route with the given alias, including excluded routes.
|
||||
func GetIncludeExcluded(alias string) (types.Route, bool) {
|
||||
if r, ok := HTTP.Get(alias); ok {
|
||||
return r, true
|
||||
}
|
||||
if r, ok := Stream.Get(alias); ok {
|
||||
return r, true
|
||||
}
|
||||
return Excluded.Get(alias)
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/logging"
|
||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
@@ -198,10 +197,9 @@ var commands = map[string]struct {
|
||||
build: func(args any) CommandHandler {
|
||||
route := args.(string)
|
||||
return TerminatingCommand(func(w http.ResponseWriter, req *http.Request) error {
|
||||
ep := entrypoint.FromCtx(req.Context())
|
||||
r, ok := ep.HTTPRoutes().Get(route)
|
||||
r, ok := routes.HTTP.Get(route)
|
||||
if !ok {
|
||||
excluded, has := ep.ExcludedRoutes().Get(route)
|
||||
excluded, has := routes.Excluded.Get(route)
|
||||
if has {
|
||||
r, ok = excluded.(types.HTTPRoute)
|
||||
}
|
||||
|
||||
@@ -3,19 +3,12 @@
|
||||
do: pass
|
||||
- name: protected
|
||||
on: |
|
||||
!path regex("(_next/static|_next/image|favicon.ico).*")
|
||||
!path glob("/api/v1/auth/*")
|
||||
!path glob("/auth/*")
|
||||
!path /icon0.svg
|
||||
!path /favicon.ico
|
||||
!path /apple-icon.png
|
||||
!path glob("/web-app-manifest-*x*.png")
|
||||
!path regex("\/assets\/(chunks\/)?[a-zA-Z0-9\-_]+\.(css|js|woff2)")
|
||||
!path regex("\/assets\/workbox-window\.prod\.es5-[a-zA-Z0-9]+\.js")
|
||||
!path regex("/workbox-[a-zA-Z0-9]+\.js")
|
||||
!path regex("[A-Za-z0-9_-]+\.(svg|png|jpg|jpeg|gif|ico|webp|woff2?|eot|ttf|otf|txt)(\?.+)?")
|
||||
!path /api/v1/version
|
||||
!path /manifest.webmanifest
|
||||
!path /sw.js
|
||||
!path /registerSW.js
|
||||
!path /manifest.json
|
||||
do: require_auth
|
||||
- name: proxy to backend
|
||||
on: path glob("/api/v1/*")
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
- name: login page
|
||||
on: path /login
|
||||
do: pass
|
||||
- name: protected
|
||||
on: |
|
||||
!path glob("@tanstack-start/*")
|
||||
!path /@react-refresh
|
||||
!path /@vite/client
|
||||
!path regex("\?token=\w{5}-\w{5}")
|
||||
!path glob("/@id/*")
|
||||
!path glob("/api/v1/auth/*")
|
||||
!path glob("/auth/*")
|
||||
!path regex("[A-Za-z0-9_\-/]+\.(css|ts|js|mjs|svg|png|jpg|jpeg|gif|ico|webp|woff2?|eot|ttf|otf|txt)(\?.+)?")
|
||||
!path /api/v1/version
|
||||
!path /manifest.webmanifest
|
||||
do: require_auth
|
||||
- name: proxy to backend
|
||||
on: path glob("/api/v1/*")
|
||||
do: proxy http://${API_ADDR}/
|
||||
- name: proxy to auth api
|
||||
on: path glob("/auth/*")
|
||||
do: |
|
||||
rewrite /auth /api/v1/auth
|
||||
proxy http://${API_ADDR}/
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/health/monitor"
|
||||
"github.com/yusing/godoxy/internal/idlewatcher"
|
||||
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/types"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
@@ -65,7 +65,6 @@ func (r *StreamRoute) Start(parent task.Parent) gperr.Error {
|
||||
if r.HealthMon != nil {
|
||||
if err := r.HealthMon.Start(r.task); err != nil {
|
||||
gperr.LogWarn("health monitor error", err, &r.l)
|
||||
r.HealthMon = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,13 +81,10 @@ func (r *StreamRoute) Start(parent task.Parent) gperr.Error {
|
||||
r.l.Info().Msg("stream closed")
|
||||
})
|
||||
|
||||
ep := entrypoint.FromCtx(parent.Context())
|
||||
if ep == nil {
|
||||
err := gperr.New("entrypoint not initialized")
|
||||
r.task.Finish(err)
|
||||
return err
|
||||
}
|
||||
ep.AddRoute(r)
|
||||
routes.Stream.Add(r)
|
||||
r.task.OnCancel("remove_route_from_stream", func() {
|
||||
routes.Stream.Del(r)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"github.com/pires/go-proxyproto"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
acl "github.com/yusing/godoxy/internal/acl/types"
|
||||
"github.com/yusing/godoxy/internal/acl"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/entrypoint"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
ioutils "github.com/yusing/goutils/io"
|
||||
"go.uber.org/atomic"
|
||||
@@ -51,17 +51,13 @@ func (s *TCPTCPStream) ListenAndServe(ctx context.Context, preDial, onRead netty
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: add to entrypoint
|
||||
|
||||
if acl := acl.FromCtx(ctx); acl != nil {
|
||||
if acl, ok := ctx.Value(acl.ContextKey{}).(*acl.Config); ok {
|
||||
log.Debug().Str("listener", s.listener.Addr().String()).Msg("wrapping listener with ACL")
|
||||
s.listener = acl.WrapTCP(s.listener)
|
||||
}
|
||||
|
||||
if ep := entrypoint.FromCtx(ctx); ep != nil {
|
||||
if proxyProto := ep.SupportProxyProtocol(); proxyProto {
|
||||
s.listener = &proxyproto.Listener{Listener: s.listener}
|
||||
}
|
||||
if proxyProto := entrypoint.ActiveConfig.Load().SupportProxyProtocol; proxyProto {
|
||||
s.listener = &proxyproto.Listener{Listener: s.listener}
|
||||
}
|
||||
|
||||
s.preDial = preDial
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
acl "github.com/yusing/godoxy/internal/acl/types"
|
||||
"github.com/yusing/godoxy/internal/acl"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
"github.com/yusing/goutils/synk"
|
||||
@@ -82,11 +82,10 @@ func (s *UDPUDPStream) ListenAndServe(ctx context.Context, preDial, onRead netty
|
||||
return
|
||||
}
|
||||
s.listener = l
|
||||
if acl := acl.FromCtx(ctx); acl != nil {
|
||||
if acl, ok := ctx.Value(acl.ContextKey{}).(*acl.Config); ok {
|
||||
log.Debug().Str("listener", s.listener.LocalAddr().String()).Msg("wrapping listener with ACL")
|
||||
s.listener = acl.WrapUDP(s.listener)
|
||||
}
|
||||
// TODO: add to entrypoint
|
||||
s.preDial = preDial
|
||||
s.onRead = onRead
|
||||
go s.listen(ctx)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
func NewTestRoute[T interface{ Helper() }](t T, task task.Parent, base *Route) (types.Route, error) {
|
||||
t.Helper()
|
||||
|
||||
err := base.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = base.Start(task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return base.impl, nil
|
||||
}
|
||||
@@ -74,19 +74,6 @@ type (
|
||||
Config *LoadBalancerConfig `json:"config"`
|
||||
Pool map[string]any `json:"pool"`
|
||||
} // @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 (
|
||||
|
||||
@@ -20,7 +20,6 @@ type (
|
||||
pool.Object
|
||||
ProviderName() string
|
||||
GetProvider() RouteProvider
|
||||
ListenURL() *nettypes.URL
|
||||
TargetURL() *nettypes.URL
|
||||
HealthMonitor() HealthMonitor
|
||||
SetHealthMonitor(m HealthMonitor)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Stage 1: deps
|
||||
FROM golang:1.25.7-alpine AS deps
|
||||
FROM golang:1.25.6-alpine AS deps
|
||||
HEALTHCHECK NONE
|
||||
|
||||
# package version does not matter
|
||||
@@ -49,7 +49,5 @@ COPY --from=builder /app/run /app/run
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
LABEL proxy.#1.healthcheck.disable=true
|
||||
|
||||
ENV LISTEN_ADDR=0.0.0.0:2375
|
||||
CMD ["/app/run"]
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/yusing/godoxy/socketproxy
|
||||
|
||||
go 1.25.7
|
||||
go 1.25.6
|
||||
|
||||
replace github.com/yusing/goutils => ../goutils
|
||||
|
||||
|
||||
Reference in New Issue
Block a user