mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 17:58:45 +02:00
refactor(config): restructured with better concurrency and error handling, reduced cross referencing
This commit is contained in:
16
cmd/main.go
16
cmd/main.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/godoxy/internal/api"
|
||||||
"github.com/yusing/godoxy/internal/auth"
|
"github.com/yusing/godoxy/internal/auth"
|
||||||
"github.com/yusing/godoxy/internal/common"
|
"github.com/yusing/godoxy/internal/common"
|
||||||
"github.com/yusing/godoxy/internal/config"
|
"github.com/yusing/godoxy/internal/config"
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||||
"github.com/yusing/godoxy/pkg"
|
"github.com/yusing/godoxy/pkg"
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
"github.com/yusing/goutils/server"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,26 +52,26 @@ func main() {
|
|||||||
prepareDirectory(dir)
|
prepareDirectory(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.Load()
|
err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gperr.LogWarn("errors in config", err)
|
gperr.LogWarn("errors in config", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Start(&config.StartServersOptions{
|
config.StartProxyServers()
|
||||||
Proxy: true,
|
|
||||||
})
|
|
||||||
if err := auth.Initialize(); err != nil {
|
if err := auth.Initialize(); err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||||
}
|
}
|
||||||
// API Handler needs to start after auth is initialized.
|
// API Handler needs to start after auth is initialized.
|
||||||
cfg.StartServers(&config.StartServersOptions{
|
server.StartServer(task.RootTask("api_server", false), server.Options{
|
||||||
API: true,
|
Name: "api",
|
||||||
|
HTTPAddr: common.APIHTTPAddr,
|
||||||
|
Handler: api.NewHandler(),
|
||||||
})
|
})
|
||||||
|
|
||||||
uptime.Poller.Start()
|
uptime.Poller.Start()
|
||||||
config.WatchChanges()
|
config.WatchChanges()
|
||||||
|
|
||||||
task.WaitExit(cfg.Value().TimeoutShutdown)
|
task.WaitExit(config.Value().TimeoutShutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareDirectory(dir string) {
|
func prepareDirectory(dir string) {
|
||||||
|
|||||||
2
goutils
2
goutils
Submodule goutils updated: 66b3d4cbeb...b2336ee8a6
@@ -9,7 +9,9 @@ import (
|
|||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/agent/pkg/certs"
|
"github.com/yusing/godoxy/agent/pkg/certs"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
|
"github.com/yusing/godoxy/internal/route/provider"
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type VerifyNewAgentRequest struct {
|
type VerifyNewAgentRequest struct {
|
||||||
@@ -57,7 +59,7 @@ func Verify(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
|
nRoutesAdded, err := verifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||||
return
|
return
|
||||||
@@ -76,3 +78,37 @@ func Verify(c *gin.Context) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
|
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
|
||||||
|
cfgState := config.ActiveState.Load()
|
||||||
|
for _, a := range cfgState.Value().Providers.Agents {
|
||||||
|
if a.Addr == host {
|
||||||
|
return 0, gperr.New("agent already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var agentCfg agent.AgentConfig
|
||||||
|
agentCfg.Addr = host
|
||||||
|
agentCfg.Runtime = containerRuntime
|
||||||
|
|
||||||
|
err := agentCfg.StartWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, gperr.Wrap(err, "failed to start agent")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := provider.NewAgentProvider(&agentCfg)
|
||||||
|
if _, loaded := cfgState.LoadOrStoreProvider(provider.String(), provider); loaded {
|
||||||
|
return 0, gperr.Errorf("provider %s already exists", provider.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// agent must be added before loading routes
|
||||||
|
agent.AddAgent(&agentCfg)
|
||||||
|
err = provider.LoadRoutes()
|
||||||
|
if err != nil {
|
||||||
|
cfgState.DeleteProvider(provider.String())
|
||||||
|
agent.RemoveAgent(&agentCfg)
|
||||||
|
return 0, gperr.Wrap(err, "failed to load routes")
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.NumRoutes(), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
apitypes "github.com/yusing/godoxy/internal/api/types"
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
|
|
||||||
|
_ "unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetFavIconRequest struct {
|
type GetFavIconRequest struct {
|
||||||
@@ -62,6 +64,7 @@ func FavIcon(c *gin.Context) {
|
|||||||
c.Data(result.StatusCode, result.ContentType(), result.Icon)
|
c.Data(result.StatusCode, result.ContentType(), result.Icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
|
||||||
func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResult, error) {
|
func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResult, error) {
|
||||||
// try with route.Icon
|
// try with route.Icon
|
||||||
r, ok := routes.HTTP.Get(alias)
|
r, ok := routes.HTTP.Get(alias)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
apitypes "github.com/yusing/godoxy/internal/api/types"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
"github.com/yusing/godoxy/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @x-id "reload"
|
// @x-id "reload"
|
||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /reload [post]
|
// @Router /reload [post]
|
||||||
func Reload(c *gin.Context) {
|
func Reload(c *gin.Context) {
|
||||||
if err := config.GetInstance().Reload(); err != nil {
|
if err := config.Reload(); err != nil {
|
||||||
c.Error(apitypes.InternalServerError(err, "failed to reload config"))
|
c.Error(apitypes.InternalServerError(err, "failed to reload config"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
statequery "github.com/yusing/godoxy/internal/config/query"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
)
|
)
|
||||||
@@ -22,12 +22,11 @@ import (
|
|||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /route/providers [get]
|
// @Router /route/providers [get]
|
||||||
func Providers(c *gin.Context) {
|
func Providers(c *gin.Context) {
|
||||||
cfg := config.GetInstance()
|
|
||||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||||
websocket.PeriodicWrite(c, 5*time.Second, func() (any, error) {
|
websocket.PeriodicWrite(c, 5*time.Second, func() (any, error) {
|
||||||
return config.GetInstance().RouteProviderList(), nil
|
return statequery.RouteProviderList(), nil
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, cfg.RouteProviderList())
|
c.JSON(http.StatusOK, statequery.RouteProviderList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apitypes "github.com/yusing/godoxy/internal/api/types"
|
apitypes "github.com/yusing/godoxy/internal/api/types"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
statequery "github.com/yusing/godoxy/internal/config/query"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ func Route(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// also search for excluded routes
|
// also search for excluded routes
|
||||||
route = config.GetInstance().SearchRoute(request.Which)
|
route = statequery.SearchRoute(request.Which)
|
||||||
if route != nil {
|
if route != nil {
|
||||||
c.JSON(http.StatusOK, route)
|
c.JSON(http.StatusOK, route)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
statequery "github.com/yusing/godoxy/internal/config/query"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
"github.com/yusing/godoxy/internal/types"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
"github.com/yusing/goutils/http/websocket"
|
"github.com/yusing/goutils/http/websocket"
|
||||||
@@ -35,10 +35,9 @@ type ProxyStats struct {
|
|||||||
// @Failure 500 {object} apitypes.ErrorResponse
|
// @Failure 500 {object} apitypes.ErrorResponse
|
||||||
// @Router /stats [get]
|
// @Router /stats [get]
|
||||||
func Stats(c *gin.Context) {
|
func Stats(c *gin.Context) {
|
||||||
cfg := config.GetInstance()
|
|
||||||
getStats := func() (any, error) {
|
getStats := func() (any, error) {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"proxies": cfg.Statistics(),
|
"proxies": statequery.GetStatistics(),
|
||||||
"uptime": int64(time.Since(startTime).Round(time.Second).Seconds()),
|
"uptime": int64(time.Since(startTime).Round(time.Second).Seconds()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
|
||||||
"github.com/yusing/godoxy/internal/route/provider"
|
|
||||||
gperr "github.com/yusing/goutils/errs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
|
|
||||||
for _, a := range cfg.value.Providers.Agents {
|
|
||||||
if a.Addr == host {
|
|
||||||
return 0, gperr.New("agent already exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
agentCfg := agent.AgentConfig{
|
|
||||||
Addr: host,
|
|
||||||
Runtime: containerRuntime,
|
|
||||||
}
|
|
||||||
err := agentCfg.StartWithCerts(cfg.Task().Context(), ca.Cert, client.Cert, client.Key)
|
|
||||||
if err != nil {
|
|
||||||
return 0, gperr.Wrap(err, "failed to start agent")
|
|
||||||
}
|
|
||||||
|
|
||||||
provider := provider.NewAgentProvider(&agentCfg)
|
|
||||||
if _, loaded := cfg.providers.LoadOrStore(provider.String(), provider); loaded {
|
|
||||||
return 0, gperr.Errorf("provider %s already exists", provider.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// agent must be added before loading routes
|
|
||||||
agent.AddAgent(&agentCfg)
|
|
||||||
err = provider.LoadRoutes()
|
|
||||||
if err != nil {
|
|
||||||
cfg.providers.Delete(provider.String())
|
|
||||||
agent.RemoveAgent(&agentCfg)
|
|
||||||
return 0, gperr.Wrap(err, "failed to load routes")
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider.NumRoutes(), nil
|
|
||||||
}
|
|
||||||
@@ -1,409 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/puzpuzpuz/xsync/v4"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
agentPkg "github.com/yusing/godoxy/agent/pkg/agent"
|
|
||||||
"github.com/yusing/godoxy/internal/api"
|
|
||||||
autocert "github.com/yusing/godoxy/internal/autocert"
|
|
||||||
"github.com/yusing/godoxy/internal/common"
|
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
|
||||||
"github.com/yusing/godoxy/internal/entrypoint"
|
|
||||||
"github.com/yusing/godoxy/internal/maxmind"
|
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
|
||||||
"github.com/yusing/godoxy/internal/proxmox"
|
|
||||||
proxy "github.com/yusing/godoxy/internal/route/provider"
|
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
value *config.Config
|
|
||||||
providers *xsync.Map[string, *proxy.Provider]
|
|
||||||
autocertProvider *autocert.Provider
|
|
||||||
entrypoint *entrypoint.Entrypoint
|
|
||||||
|
|
||||||
task *task.Task
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
cfgWatcher watcher.Watcher
|
|
||||||
reloadMu sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
const configEventFlushInterval = 500 * time.Millisecond
|
|
||||||
|
|
||||||
const (
|
|
||||||
cfgRenameWarn = `Config file renamed, not reloading.
|
|
||||||
Make sure you rename it back before next time you start.`
|
|
||||||
cfgDeleteWarn = `Config file deleted, not reloading.
|
|
||||||
You may run "ls-config" to show or dump the current config.`
|
|
||||||
)
|
|
||||||
|
|
||||||
var Validate = config.Validate
|
|
||||||
|
|
||||||
func newConfig() *Config {
|
|
||||||
return &Config{
|
|
||||||
value: config.DefaultConfig(),
|
|
||||||
providers: xsync.NewMap[string, *proxy.Provider](),
|
|
||||||
entrypoint: entrypoint.NewEntrypoint(),
|
|
||||||
task: task.RootTask("config", false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Load() (*Config, gperr.Error) {
|
|
||||||
if config.HasInstance() {
|
|
||||||
panic(errors.New("config already loaded"))
|
|
||||||
}
|
|
||||||
cfg := newConfig()
|
|
||||||
config.SetInstance(cfg)
|
|
||||||
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
|
|
||||||
return cfg, cfg.load()
|
|
||||||
}
|
|
||||||
|
|
||||||
func MatchDomains() []string {
|
|
||||||
return config.GetInstance().Value().MatchDomains
|
|
||||||
}
|
|
||||||
|
|
||||||
func WatchChanges() {
|
|
||||||
t := task.RootTask("config_watcher", true)
|
|
||||||
eventQueue := events.NewEventQueue(
|
|
||||||
t,
|
|
||||||
configEventFlushInterval,
|
|
||||||
OnConfigChange,
|
|
||||||
func(err gperr.Error) {
|
|
||||||
gperr.LogError("config reload error", err)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
eventQueue.Start(cfgWatcher.Events(t.Context()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func OnConfigChange(ev []events.Event) {
|
|
||||||
// no matter how many events during the interval
|
|
||||||
// just reload once and check the last event
|
|
||||||
switch ev[len(ev)-1].Action {
|
|
||||||
case events.ActionFileRenamed:
|
|
||||||
log.Warn().Msg(cfgRenameWarn)
|
|
||||||
return
|
|
||||||
case events.ActionFileDeleted:
|
|
||||||
log.Warn().Msg(cfgDeleteWarn)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := Reload(); err != nil {
|
|
||||||
// recovered in event queue
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Reload() gperr.Error {
|
|
||||||
// avoid race between config change and API reload request
|
|
||||||
reloadMu.Lock()
|
|
||||||
defer reloadMu.Unlock()
|
|
||||||
|
|
||||||
newCfg := newConfig()
|
|
||||||
err := newCfg.load()
|
|
||||||
if err != nil {
|
|
||||||
newCfg.task.FinishAndWait(err)
|
|
||||||
return gperr.New(ansi.Warning("using last config")).With(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cancel all current subtasks -> wait
|
|
||||||
// -> replace config -> start new subtasks
|
|
||||||
config.GetInstance().(*Config).Task().FinishAndWait("config changed")
|
|
||||||
newCfg.Start(StartAllServers)
|
|
||||||
config.SetInstance(newCfg)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) Value() *config.Config {
|
|
||||||
return cfg.value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) Reload() gperr.Error {
|
|
||||||
return Reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AutoCertProvider returns the autocert provider.
|
|
||||||
//
|
|
||||||
// If the autocert provider is not configured, it returns nil.
|
|
||||||
func (cfg *Config) AutoCertProvider() *autocert.Provider {
|
|
||||||
return cfg.autocertProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) Task() *task.Task {
|
|
||||||
return cfg.task
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) Context() context.Context {
|
|
||||||
return cfg.task.Context()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) Start(opts ...*StartServersOptions) {
|
|
||||||
cfg.StartAutoCert()
|
|
||||||
cfg.StartProxyProviders()
|
|
||||||
cfg.StartServers(opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) StartAutoCert() {
|
|
||||||
autocert := cfg.autocertProvider
|
|
||||||
if autocert == nil {
|
|
||||||
log.Info().Msg("autocert not configured")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := autocert.Setup(); err != nil {
|
|
||||||
gperr.LogFatal("autocert setup error", err)
|
|
||||||
} else {
|
|
||||||
autocert.ScheduleRenewal(cfg.task)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) StartProxyProviders() {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
errs := gperr.NewBuilderWithConcurrency()
|
|
||||||
for _, p := range cfg.providers.Range {
|
|
||||||
wg.Go(func() {
|
|
||||||
if err := p.Start(cfg.task); err != nil {
|
|
||||||
errs.Add(err.Subject(p.String()))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if err := errs.Error(); err != nil {
|
|
||||||
gperr.LogError("route provider errors", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type StartServersOptions struct {
|
|
||||||
Proxy, API bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var StartAllServers = &StartServersOptions{true, true}
|
|
||||||
|
|
||||||
func (cfg *Config) StartServers(opts ...*StartServersOptions) {
|
|
||||||
if len(opts) == 0 {
|
|
||||||
opts = append(opts, &StartServersOptions{})
|
|
||||||
}
|
|
||||||
opt := opts[0]
|
|
||||||
if opt.Proxy {
|
|
||||||
server.StartServer(cfg.task, server.Options{
|
|
||||||
Name: "proxy",
|
|
||||||
CertProvider: cfg.AutoCertProvider(),
|
|
||||||
HTTPAddr: common.ProxyHTTPAddr,
|
|
||||||
HTTPSAddr: common.ProxyHTTPSAddr,
|
|
||||||
Handler: cfg.entrypoint,
|
|
||||||
ACL: cfg.value.ACL,
|
|
||||||
SupportProxyProtocol: cfg.value.Entrypoint.SupportProxyProtocol,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if opt.API {
|
|
||||||
server.StartServer(cfg.task, server.Options{
|
|
||||||
Name: "api",
|
|
||||||
CertProvider: cfg.AutoCertProvider(),
|
|
||||||
HTTPAddr: common.APIHTTPAddr,
|
|
||||||
Handler: api.NewHandler(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) load() gperr.Error {
|
|
||||||
const errMsg = "config load error"
|
|
||||||
|
|
||||||
data, err := os.ReadFile(common.ConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
log.Warn().Msg("config file not found, using default config")
|
|
||||||
cfg.value = config.DefaultConfig()
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
gperr.LogFatal(errMsg, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
model := config.DefaultConfig()
|
|
||||||
if err := serialization.UnmarshalValidateYAML(data, model); err != nil {
|
|
||||||
gperr.LogFatal(errMsg, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// errors are non fatal below
|
|
||||||
errs := gperr.NewBuilder(errMsg)
|
|
||||||
errs.Add(cfg.entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
|
|
||||||
errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
|
|
||||||
errs.Add(cfg.initMaxMind(model.Providers.MaxMind))
|
|
||||||
cfg.initNotification(model.Providers.Notification)
|
|
||||||
errs.Add(cfg.initAutoCert(model.AutoCert))
|
|
||||||
errs.Add(cfg.initProxmox(model.Providers.Proxmox))
|
|
||||||
errs.Add(cfg.loadRouteProviders(&model.Providers))
|
|
||||||
|
|
||||||
cfg.value = model
|
|
||||||
for i, domain := range model.MatchDomains {
|
|
||||||
if !strings.HasPrefix(domain, ".") {
|
|
||||||
model.MatchDomains[i] = "." + domain
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cfg.entrypoint.SetFindRouteDomains(model.MatchDomains)
|
|
||||||
if model.ACL.Valid() {
|
|
||||||
err := model.ACL.Start(cfg.task)
|
|
||||||
if err != nil {
|
|
||||||
errs.Add(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if errs.HasError() {
|
|
||||||
notif.Notify(¬if.LogMessage{
|
|
||||||
Level: zerolog.ErrorLevel,
|
|
||||||
Title: "Config Reload Error",
|
|
||||||
Body: notif.ErrorBody{Error: errs.Error()},
|
|
||||||
})
|
|
||||||
return errs.Error()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) initMaxMind(maxmindCfg *maxmind.Config) gperr.Error {
|
|
||||||
if maxmindCfg != nil {
|
|
||||||
return maxmind.SetInstance(cfg.task, maxmindCfg)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) initNotification(notifCfg []notif.NotificationConfig) {
|
|
||||||
if len(notifCfg) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dispatcher := notif.StartNotifDispatcher(cfg.task)
|
|
||||||
for _, notifier := range notifCfg {
|
|
||||||
dispatcher.RegisterProvider(¬ifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) initAutoCert(autocertCfg *autocert.Config) gperr.Error {
|
|
||||||
if cfg.autocertProvider != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if autocertCfg == nil {
|
|
||||||
autocertCfg = new(autocert.Config)
|
|
||||||
}
|
|
||||||
|
|
||||||
user, legoCfg, err := autocertCfg.GetLegoConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.autocertProvider = autocert.NewProvider(autocertCfg, user, legoCfg)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) initProxmox(proxmoxCfg []proxmox.Config) gperr.Error {
|
|
||||||
proxmox.Clients.Clear()
|
|
||||||
errs := gperr.NewBuilder()
|
|
||||||
for _, cfg := range proxmoxCfg {
|
|
||||||
if err := cfg.Init(); err != nil {
|
|
||||||
errs.Add(err.Subject(cfg.URL))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errs.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) storeProvider(p *proxy.Provider) {
|
|
||||||
cfg.providers.Store(p.String(), p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) loadRouteProviders(providers *config.Providers) gperr.Error {
|
|
||||||
errs := gperr.NewBuilderWithConcurrency("route provider errors")
|
|
||||||
results := gperr.NewBuilder("loaded route providers")
|
|
||||||
|
|
||||||
agentPkg.RemoveAllAgents()
|
|
||||||
|
|
||||||
numProviders := len(providers.Agents) + len(providers.Files) + len(providers.Docker)
|
|
||||||
providersCh := make(chan *proxy.Provider, numProviders)
|
|
||||||
|
|
||||||
// start providers concurrently
|
|
||||||
var providersConsumer sync.WaitGroup
|
|
||||||
providersConsumer.Go(func() {
|
|
||||||
for p := range providersCh {
|
|
||||||
if actual, loaded := cfg.providers.LoadOrStore(p.String(), p); loaded {
|
|
||||||
errs.Add(gperr.Errorf("provider %s already exists, first: %s, second: %s", p.String(), actual.GetType(), p.GetType()))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cfg.storeProvider(p)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var providersProducer sync.WaitGroup
|
|
||||||
for _, agent := range providers.Agents {
|
|
||||||
providersProducer.Go(func() {
|
|
||||||
if err := agent.Start(cfg.task.Context()); err != nil {
|
|
||||||
errs.Add(gperr.PrependSubject(agent.String(), err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
agentPkg.AddAgent(agent)
|
|
||||||
p := proxy.NewAgentProvider(agent)
|
|
||||||
providersCh <- p
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, filename := range providers.Files {
|
|
||||||
providersProducer.Go(func() {
|
|
||||||
p, err := proxy.NewFileProvider(filename)
|
|
||||||
if err != nil {
|
|
||||||
errs.Add(gperr.PrependSubject(filename, err))
|
|
||||||
} else {
|
|
||||||
providersCh <- p
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, dockerHost := range providers.Docker {
|
|
||||||
providersProducer.Go(func() {
|
|
||||||
providersCh <- proxy.NewDockerProvider(name, dockerHost)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
providersProducer.Wait()
|
|
||||||
|
|
||||||
close(providersCh)
|
|
||||||
providersConsumer.Wait()
|
|
||||||
|
|
||||||
lenLongestName := 0
|
|
||||||
for k := range cfg.providers.Range {
|
|
||||||
if len(k) > lenLongestName {
|
|
||||||
lenLongestName = len(k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results.EnableConcurrency()
|
|
||||||
|
|
||||||
// load routes concurrently
|
|
||||||
var providersLoader sync.WaitGroup
|
|
||||||
for _, p := range cfg.providers.Range {
|
|
||||||
providersLoader.Go(func() {
|
|
||||||
if err := p.LoadRoutes(); err != nil {
|
|
||||||
errs.Add(err.Subject(p.String()))
|
|
||||||
}
|
|
||||||
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
providersLoader.Wait()
|
|
||||||
|
|
||||||
log.Info().Msg(results.String())
|
|
||||||
return errs.Error()
|
|
||||||
}
|
|
||||||
121
internal/config/events.go
Normal file
121
internal/config/events.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/godoxy/internal/common"
|
||||||
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cfgWatcher watcher.Watcher
|
||||||
|
reloadMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
const configEventFlushInterval = 500 * time.Millisecond
|
||||||
|
|
||||||
|
const (
|
||||||
|
cfgRenameWarn = `Config file renamed, not reloading.
|
||||||
|
Make sure you rename it back before next time you start.`
|
||||||
|
cfgDeleteWarn = `Config file deleted, not reloading.
|
||||||
|
You may run "ls-config" to show or dump the current config.`
|
||||||
|
)
|
||||||
|
|
||||||
|
func Load() error {
|
||||||
|
if HasState() {
|
||||||
|
panic(errors.New("config already loaded"))
|
||||||
|
}
|
||||||
|
state := NewState()
|
||||||
|
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
|
||||||
|
|
||||||
|
err := state.InitFromFileOrExit(common.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
notifyError("init", err)
|
||||||
|
}
|
||||||
|
SetState(state)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func notifyError(action string, err error) {
|
||||||
|
notif.Notify(¬if.LogMessage{
|
||||||
|
Level: zerolog.ErrorLevel,
|
||||||
|
Title: fmt.Sprintf("Config %s Error", action),
|
||||||
|
Body: notif.ErrorBody(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reload() gperr.Error {
|
||||||
|
// avoid race between config change and API reload request
|
||||||
|
reloadMu.Lock()
|
||||||
|
defer reloadMu.Unlock()
|
||||||
|
|
||||||
|
newState := NewState()
|
||||||
|
err := newState.InitFromFileOrExit(common.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
newState.Task().FinishAndWait(err)
|
||||||
|
notifyError("reload", err)
|
||||||
|
return gperr.New(ansi.Warning("using last config")).With(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancel all current subtasks -> wait
|
||||||
|
// -> replace config -> start new subtasks
|
||||||
|
GetState().Task().FinishAndWait("config changed")
|
||||||
|
SetState(newState)
|
||||||
|
StartProxyServers()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WatchChanges() {
|
||||||
|
t := task.RootTask("config_watcher", true)
|
||||||
|
eventQueue := events.NewEventQueue(
|
||||||
|
t,
|
||||||
|
configEventFlushInterval,
|
||||||
|
OnConfigChange,
|
||||||
|
func(err gperr.Error) {
|
||||||
|
gperr.LogError("config reload error", err)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
eventQueue.Start(cfgWatcher.Events(t.Context()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnConfigChange(ev []events.Event) {
|
||||||
|
// no matter how many events during the interval
|
||||||
|
// just reload once and check the last event
|
||||||
|
switch ev[len(ev)-1].Action {
|
||||||
|
case events.ActionFileRenamed:
|
||||||
|
log.Warn().Msg(cfgRenameWarn)
|
||||||
|
return
|
||||||
|
case events.ActionFileDeleted:
|
||||||
|
log.Warn().Msg(cfgDeleteWarn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Reload(); err != nil {
|
||||||
|
// recovered in event queue
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
|
||||||
"github.com/yusing/godoxy/internal/route/provider"
|
|
||||||
"github.com/yusing/godoxy/internal/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cfg *Config) DumpRouteProviders() map[string]*provider.Provider {
|
|
||||||
entries := make(map[string]*provider.Provider, cfg.providers.Size())
|
|
||||||
for _, p := range cfg.providers.Range {
|
|
||||||
entries[p.ShortName()] = p
|
|
||||||
}
|
|
||||||
return entries
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) RouteProviderList() []config.RouteProviderListResponse {
|
|
||||||
list := make([]config.RouteProviderListResponse, 0, cfg.providers.Size())
|
|
||||||
for _, p := range cfg.providers.Range {
|
|
||||||
list = append(list, config.RouteProviderListResponse{
|
|
||||||
ShortName: p.ShortName(),
|
|
||||||
FullName: p.String(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) SearchRoute(alias string) types.Route {
|
|
||||||
for _, p := range cfg.providers.Range {
|
|
||||||
if r, ok := p.GetRoute(alias); ok {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) Statistics() map[string]any {
|
|
||||||
var rps, streams types.RouteStats
|
|
||||||
var total uint16
|
|
||||||
providerStats := make(map[string]types.ProviderStats)
|
|
||||||
|
|
||||||
for _, p := range cfg.providers.Range {
|
|
||||||
stats := p.Statistics()
|
|
||||||
providerStats[p.ShortName()] = stats
|
|
||||||
rps.AddOther(stats.RPs)
|
|
||||||
streams.AddOther(stats.Streams)
|
|
||||||
total += stats.RPs.Total + stats.Streams.Total
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]any{
|
|
||||||
"total": total,
|
|
||||||
"reverse_proxies": rps,
|
|
||||||
"streams": streams,
|
|
||||||
"providers": providerStats,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
42
internal/config/query/query.go
Normal file
42
internal/config/query/query.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package statequery
|
||||||
|
|
||||||
|
import (
|
||||||
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RouteProviderListResponse struct {
|
||||||
|
ShortName string `json:"short_name"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
} // @name RouteProvider
|
||||||
|
|
||||||
|
func DumpRouteProviders() map[string]types.RouteProvider {
|
||||||
|
state := config.ActiveState.Load()
|
||||||
|
entries := make(map[string]types.RouteProvider, state.NumProviders())
|
||||||
|
for _, p := range state.IterProviders() {
|
||||||
|
entries[p.ShortName()] = p
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func RouteProviderList() []RouteProviderListResponse {
|
||||||
|
state := config.ActiveState.Load()
|
||||||
|
list := make([]RouteProviderListResponse, 0, state.NumProviders())
|
||||||
|
for _, p := range state.IterProviders() {
|
||||||
|
list = append(list, RouteProviderListResponse{
|
||||||
|
ShortName: p.ShortName(),
|
||||||
|
FullName: p.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchRoute(alias string) types.Route {
|
||||||
|
state := config.ActiveState.Load()
|
||||||
|
for _, p := range state.IterProviders() {
|
||||||
|
if r, ok := p.GetRoute(alias); ok {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
38
internal/config/query/stats.go
Normal file
38
internal/config/query/stats.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package statequery
|
||||||
|
|
||||||
|
import (
|
||||||
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Statistics struct {
|
||||||
|
Total uint16 `json:"total"`
|
||||||
|
ReverseProxies types.RouteStats `json:"reverse_proxies"`
|
||||||
|
Streams types.RouteStats `json:"streams"`
|
||||||
|
Providers map[string]types.ProviderStats `json:"providers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStatistics() Statistics {
|
||||||
|
state := config.ActiveState.Load()
|
||||||
|
|
||||||
|
var (
|
||||||
|
rps, streams types.RouteStats
|
||||||
|
total uint16
|
||||||
|
providerStats = make(map[string]types.ProviderStats)
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, p := range state.IterProviders() {
|
||||||
|
stats := p.Statistics()
|
||||||
|
providerStats[p.ShortName()] = stats
|
||||||
|
rps.AddOther(stats.RPs)
|
||||||
|
streams.AddOther(stats.Streams)
|
||||||
|
total += stats.RPs.Total + stats.Streams.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
return Statistics{
|
||||||
|
Total: total,
|
||||||
|
ReverseProxies: rps,
|
||||||
|
Streams: streams,
|
||||||
|
Providers: providerStats,
|
||||||
|
}
|
||||||
|
}
|
||||||
330
internal/config/state.go
Normal file
330
internal/config/state.go
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/puzpuzpuz/xsync/v4"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
|
"github.com/yusing/godoxy/internal/acl"
|
||||||
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
|
"github.com/yusing/godoxy/internal/common"
|
||||||
|
config "github.com/yusing/godoxy/internal/config/types"
|
||||||
|
"github.com/yusing/godoxy/internal/entrypoint"
|
||||||
|
homepage "github.com/yusing/godoxy/internal/homepage/types"
|
||||||
|
"github.com/yusing/godoxy/internal/maxmind"
|
||||||
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
|
route "github.com/yusing/godoxy/internal/route/provider"
|
||||||
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
|
"github.com/yusing/goutils/server"
|
||||||
|
"github.com/yusing/goutils/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
config.Config
|
||||||
|
|
||||||
|
providers *xsync.Map[string, types.RouteProvider]
|
||||||
|
autocertProvider *autocert.Provider
|
||||||
|
entrypoint entrypoint.Entrypoint
|
||||||
|
|
||||||
|
task *task.Task
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewState() config.State {
|
||||||
|
return &state{
|
||||||
|
providers: xsync.NewMap[string, types.RouteProvider](),
|
||||||
|
entrypoint: entrypoint.NewEntrypoint(),
|
||||||
|
task: task.RootTask("config", false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stateMu sync.RWMutex
|
||||||
|
|
||||||
|
func GetState() config.State {
|
||||||
|
return config.ActiveState.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetState(state config.State) {
|
||||||
|
stateMu.Lock()
|
||||||
|
defer stateMu.Unlock()
|
||||||
|
|
||||||
|
cfg := state.Value()
|
||||||
|
config.ActiveConfig.Store(cfg)
|
||||||
|
config.ActiveState.Store(state)
|
||||||
|
acl.ActiveConfig.Store(cfg.ACL)
|
||||||
|
entrypoint.ActiveConfig.Store(&cfg.Entrypoint)
|
||||||
|
homepage.ActiveConfig.Store(&cfg.Homepage)
|
||||||
|
autocert.ActiveProvider.Store(state.AutoCertProvider().(*autocert.Provider))
|
||||||
|
}
|
||||||
|
|
||||||
|
func HasState() bool {
|
||||||
|
return config.ActiveState.Load() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Value() *config.Config {
|
||||||
|
return config.ActiveConfig.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) InitFromFileOrExit(filename string) error {
|
||||||
|
data, err := os.ReadFile(common.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Warn().Msg("config file not found, using default config")
|
||||||
|
state.Config = *config.DefaultConfig()
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
gperr.LogFatal("config init error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state.Init(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) Init(data []byte) error {
|
||||||
|
err := serialization.UnmarshalValidateYAML(data, &state.Config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
g := gperr.NewGroup("config load error")
|
||||||
|
g.Go(state.initMaxMind)
|
||||||
|
g.Go(state.initProxmox)
|
||||||
|
g.Go(state.loadRouteProviders)
|
||||||
|
g.Go(state.initAutoCert)
|
||||||
|
|
||||||
|
errs := g.Wait()
|
||||||
|
// these won't benefit from running on goroutines
|
||||||
|
errs.Add(state.initNotification())
|
||||||
|
errs.Add(state.initAccessLogger())
|
||||||
|
errs.Add(state.initEntrypoint())
|
||||||
|
// this must be run after loadRouteProviders
|
||||||
|
errs.Add(state.startRouteProviders())
|
||||||
|
return errs.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) Task() *task.Task {
|
||||||
|
return state.task
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) Context() context.Context {
|
||||||
|
return state.task.Context()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) Value() *config.Config {
|
||||||
|
return &state.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) EntrypointHandler() http.Handler {
|
||||||
|
return &state.entrypoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoCertProvider returns the autocert provider.
|
||||||
|
//
|
||||||
|
// If the autocert provider is not configured, it returns nil.
|
||||||
|
func (state *state) AutoCertProvider() server.CertProvider {
|
||||||
|
return state.autocertProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool) {
|
||||||
|
actual, loaded = state.providers.LoadOrStore(key, value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) DeleteProvider(key string) {
|
||||||
|
state.providers.Delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) IterProviders() iter.Seq2[string, types.RouteProvider] {
|
||||||
|
return func(yield func(string, types.RouteProvider) bool) {
|
||||||
|
for k, v := range state.providers.Range {
|
||||||
|
if !yield(k, v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) NumProviders() int {
|
||||||
|
return state.providers.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
// this one is connection level access logger, different from entrypoint access logger
|
||||||
|
func (state *state) initAccessLogger() error {
|
||||||
|
if !state.ACL.Valid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return state.ACL.Start(state.task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) initEntrypoint() error {
|
||||||
|
epCfg := state.Entrypoint
|
||||||
|
matchDomains := state.MatchDomains
|
||||||
|
|
||||||
|
state.entrypoint.SetFindRouteDomains(matchDomains)
|
||||||
|
state.entrypoint.SetCatchAllRules(epCfg.Rules.CatchAll)
|
||||||
|
state.entrypoint.SetNotFoundRules(epCfg.Rules.NotFound)
|
||||||
|
|
||||||
|
errs := gperr.NewBuilder("entrypoint error")
|
||||||
|
errs.Add(state.entrypoint.SetMiddlewares(epCfg.Middlewares))
|
||||||
|
errs.Add(state.entrypoint.SetAccessLogger(state.task, epCfg.AccessLog))
|
||||||
|
return errs.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) initMaxMind() error {
|
||||||
|
maxmindCfg := state.Providers.MaxMind
|
||||||
|
if maxmindCfg != nil {
|
||||||
|
return maxmind.SetInstance(state.task, maxmindCfg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) initNotification() error {
|
||||||
|
notifCfg := state.Providers.Notification
|
||||||
|
if len(notifCfg) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcher := notif.StartNotifDispatcher(state.task)
|
||||||
|
for _, notifier := range notifCfg {
|
||||||
|
dispatcher.RegisterProvider(¬ifier)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) initAutoCert() error {
|
||||||
|
autocertCfg := state.AutoCert
|
||||||
|
if autocertCfg == nil {
|
||||||
|
autocertCfg = new(autocert.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, legoCfg, err := autocertCfg.GetLegoConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
state.autocertProvider = autocert.NewProvider(autocertCfg, user, legoCfg)
|
||||||
|
if err := state.autocertProvider.Setup(); err != nil {
|
||||||
|
return fmt.Errorf("autocert error: %w", err)
|
||||||
|
} else {
|
||||||
|
state.autocertProvider.ScheduleRenewal(state.task)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) initProxmox() error {
|
||||||
|
proxmoxCfg := state.Providers.Proxmox
|
||||||
|
if len(proxmoxCfg) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := gperr.NewBuilder()
|
||||||
|
for _, cfg := range proxmoxCfg {
|
||||||
|
if err := cfg.Init(); err != nil {
|
||||||
|
errs.Add(err.Subject(cfg.URL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) storeProvider(p types.RouteProvider) {
|
||||||
|
state.providers.Store(p.String(), p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) loadRouteProviders() error {
|
||||||
|
providers := &state.Providers
|
||||||
|
errs := gperr.NewBuilderWithConcurrency("route provider errors")
|
||||||
|
results := gperr.NewBuilder("loaded route providers")
|
||||||
|
|
||||||
|
agent.RemoveAllAgents()
|
||||||
|
|
||||||
|
numProviders := len(providers.Agents) + len(providers.Files) + len(providers.Docker)
|
||||||
|
providersCh := make(chan types.RouteProvider, numProviders)
|
||||||
|
|
||||||
|
// start providers concurrently
|
||||||
|
var providersConsumer sync.WaitGroup
|
||||||
|
providersConsumer.Go(func() {
|
||||||
|
for p := range providersCh {
|
||||||
|
if actual, loaded := state.providers.LoadOrStore(p.String(), p); loaded {
|
||||||
|
errs.Add(gperr.Errorf("provider %s already exists, first: %s, second: %s", p.String(), actual.GetType(), p.GetType()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
state.storeProvider(p)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var providersProducer sync.WaitGroup
|
||||||
|
for _, a := range providers.Agents {
|
||||||
|
providersProducer.Go(func() {
|
||||||
|
if err := a.Start(state.task.Context()); err != nil {
|
||||||
|
errs.Add(gperr.PrependSubject(a.String(), err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
agent.AddAgent(&a)
|
||||||
|
p := route.NewAgentProvider(&a)
|
||||||
|
providersCh <- p
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filename := range providers.Files {
|
||||||
|
providersProducer.Go(func() {
|
||||||
|
p, err := route.NewFileProvider(filename)
|
||||||
|
if err != nil {
|
||||||
|
errs.Add(gperr.PrependSubject(filename, err))
|
||||||
|
} else {
|
||||||
|
providersCh <- p
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, dockerHost := range providers.Docker {
|
||||||
|
providersProducer.Go(func() {
|
||||||
|
providersCh <- route.NewDockerProvider(name, dockerHost)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
providersProducer.Wait()
|
||||||
|
|
||||||
|
close(providersCh)
|
||||||
|
providersConsumer.Wait()
|
||||||
|
|
||||||
|
lenLongestName := 0
|
||||||
|
for k := range state.providers.Range {
|
||||||
|
if len(k) > lenLongestName {
|
||||||
|
lenLongestName = len(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.EnableConcurrency()
|
||||||
|
|
||||||
|
// load routes concurrently
|
||||||
|
var providersLoader sync.WaitGroup
|
||||||
|
for _, p := range state.providers.Range {
|
||||||
|
providersLoader.Go(func() {
|
||||||
|
if err := p.LoadRoutes(); err != nil {
|
||||||
|
errs.Add(err.Subject(p.String()))
|
||||||
|
}
|
||||||
|
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
providersLoader.Wait()
|
||||||
|
|
||||||
|
log.Info().Msg(results.String())
|
||||||
|
return errs.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *state) startRouteProviders() error {
|
||||||
|
errs := gperr.NewGroup("provider errors")
|
||||||
|
for _, p := range state.providers.Range {
|
||||||
|
errs.Go(func() error {
|
||||||
|
return p.Start(state.Task())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return errs.Wait().Error()
|
||||||
|
}
|
||||||
@@ -1,95 +1,47 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/internal/acl"
|
"github.com/yusing/godoxy/internal/acl"
|
||||||
"github.com/yusing/godoxy/internal/autocert"
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||||
|
homepage "github.com/yusing/godoxy/internal/homepage/types"
|
||||||
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
|
||||||
"github.com/yusing/godoxy/internal/notif"
|
"github.com/yusing/godoxy/internal/notif"
|
||||||
"github.com/yusing/godoxy/internal/proxmox"
|
"github.com/yusing/godoxy/internal/proxmox"
|
||||||
"github.com/yusing/godoxy/internal/serialization"
|
"github.com/yusing/godoxy/internal/serialization"
|
||||||
"github.com/yusing/godoxy/internal/types"
|
|
||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Config struct {
|
Config struct {
|
||||||
ACL *acl.Config `json:"acl"`
|
ACL *acl.Config `json:"acl"`
|
||||||
AutoCert *autocert.Config `json:"autocert"`
|
AutoCert *autocert.Config `json:"autocert"`
|
||||||
Entrypoint Entrypoint `json:"entrypoint"`
|
Entrypoint entrypoint.Config `json:"entrypoint"`
|
||||||
Providers Providers `json:"providers"`
|
Providers Providers `json:"providers"`
|
||||||
MatchDomains []string `json:"match_domains" validate:"domain_name"`
|
MatchDomains []string `json:"match_domains" validate:"domain_name"`
|
||||||
Homepage HomepageConfig `json:"homepage"`
|
Homepage homepage.Config `json:"homepage"`
|
||||||
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
|
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
|
||||||
}
|
}
|
||||||
Providers struct {
|
Providers struct {
|
||||||
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
|
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
|
||||||
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"`
|
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"`
|
||||||
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
|
Agents []agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
|
||||||
Notification []notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
|
Notification []notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
|
||||||
Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"`
|
Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"`
|
||||||
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
|
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
|
||||||
}
|
}
|
||||||
Entrypoint struct {
|
|
||||||
SupportProxyProtocol bool `json:"support_proxy_protocol"`
|
|
||||||
Middlewares []map[string]any `json:"middlewares"`
|
|
||||||
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
|
|
||||||
}
|
|
||||||
HomepageConfig struct {
|
|
||||||
UseDefaultCategories bool `json:"use_default_categories"`
|
|
||||||
}
|
|
||||||
RouteProviderListResponse struct {
|
|
||||||
ShortName string `json:"short_name"`
|
|
||||||
FullName string `json:"full_name"`
|
|
||||||
} // @name RouteProvider
|
|
||||||
ConfigInstance interface {
|
|
||||||
Value() *Config
|
|
||||||
Reload() gperr.Error
|
|
||||||
Statistics() map[string]any
|
|
||||||
RouteProviderList() []RouteProviderListResponse
|
|
||||||
SearchRoute(alias string) types.Route
|
|
||||||
Context() context.Context
|
|
||||||
VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error)
|
|
||||||
AutoCertProvider() *autocert.Provider
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// nil-safe
|
||||||
instance ConfigInstance
|
var ActiveConfig atomic.Pointer[Config]
|
||||||
instanceMu sync.RWMutex
|
|
||||||
)
|
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
func init() {
|
||||||
return &Config{
|
ActiveConfig.Store(DefaultConfig())
|
||||||
TimeoutShutdown: 3,
|
|
||||||
Homepage: HomepageConfig{
|
|
||||||
UseDefaultCategories: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetInstance() ConfigInstance {
|
|
||||||
instanceMu.RLock()
|
|
||||||
defer instanceMu.RUnlock()
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetInstance(cfg ConfigInstance) {
|
|
||||||
instanceMu.Lock()
|
|
||||||
defer instanceMu.Unlock()
|
|
||||||
instance = cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func HasInstance() bool {
|
|
||||||
instanceMu.RLock()
|
|
||||||
defer instanceMu.RUnlock()
|
|
||||||
return instance != nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Validate(data []byte) gperr.Error {
|
func Validate(data []byte) gperr.Error {
|
||||||
@@ -97,6 +49,15 @@ func Validate(data []byte) gperr.Error {
|
|||||||
return serialization.UnmarshalValidateYAML(data, &model)
|
return serialization.UnmarshalValidateYAML(data, &model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
TimeoutShutdown: 3,
|
||||||
|
Homepage: homepage.Config{
|
||||||
|
UseDefaultCategories: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)
|
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
34
internal/config/types/state.go
Normal file
34
internal/config/types/state.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"iter"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/yusing/godoxy/internal/types"
|
||||||
|
"github.com/yusing/godoxy/internal/utils/atomic"
|
||||||
|
"github.com/yusing/goutils/server"
|
||||||
|
"github.com/yusing/goutils/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
type State interface {
|
||||||
|
// InitFromFileOrExit logs and exits on critical errors, non-critical ones will be returned
|
||||||
|
InitFromFileOrExit(filename string) error
|
||||||
|
Init(data []byte) error
|
||||||
|
|
||||||
|
Task() *task.Task
|
||||||
|
Context() context.Context
|
||||||
|
|
||||||
|
Value() *Config
|
||||||
|
|
||||||
|
EntrypointHandler() http.Handler
|
||||||
|
AutoCertProvider() server.CertProvider
|
||||||
|
|
||||||
|
LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool)
|
||||||
|
DeleteProvider(key string)
|
||||||
|
IterProviders() iter.Seq2[string, types.RouteProvider]
|
||||||
|
NumProviders() int
|
||||||
|
}
|
||||||
|
|
||||||
|
// could be nil
|
||||||
|
var ActiveState atomic.Value[State]
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
package entrypoint
|
package entrypoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||||
@@ -16,15 +15,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Entrypoint struct {
|
type Entrypoint struct {
|
||||||
middleware *middleware.Middleware
|
middleware *middleware.Middleware
|
||||||
accessLogger *accesslog.AccessLogger
|
accessLogger *accesslog.AccessLogger
|
||||||
findRouteFunc func(host string) (types.HTTPRoute, error)
|
findRouteFunc func(host string) types.HTTPRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrNoSuchRoute = errors.New("no such route")
|
// nil-safe
|
||||||
|
var ActiveConfig atomic.Pointer[entrypoint.Config]
|
||||||
|
|
||||||
func NewEntrypoint() *Entrypoint {
|
func init() {
|
||||||
return &Entrypoint{
|
// make sure it's not nil
|
||||||
|
ActiveConfig.Store(&entrypoint.Config{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEntrypoint() Entrypoint {
|
||||||
|
return Entrypoint{
|
||||||
findRouteFunc: findRouteAnyDomain,
|
findRouteFunc: findRouteAnyDomain,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,8 +77,10 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
w = accesslog.NewResponseRecorder(w)
|
w = accesslog.NewResponseRecorder(w)
|
||||||
defer ep.accessLogger.Log(r, w.(*accesslog.ResponseRecorder).Response())
|
defer ep.accessLogger.Log(r, w.(*accesslog.ResponseRecorder).Response())
|
||||||
}
|
}
|
||||||
route, err := ep.findRouteFunc(r.Host)
|
|
||||||
if err == nil {
|
route := ep.findRouteFunc(r.Host)
|
||||||
|
switch {
|
||||||
|
case route != nil:
|
||||||
r = routes.WithRouteContext(r, route)
|
r = routes.WithRouteContext(r, route)
|
||||||
if ep.middleware != nil {
|
if ep.middleware != nil {
|
||||||
ep.middleware.ServeHTTP(route.ServeHTTP, w, r)
|
ep.middleware.ServeHTTP(route.ServeHTTP, w, r)
|
||||||
@@ -87,11 +94,11 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Then scraper / scanners will know the subdomain is invalid.
|
// 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.
|
// With StatusNotFound, they won't know whether it's the path, or the subdomain that is invalid.
|
||||||
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
|
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
|
||||||
log.Err(err).
|
log.Error().
|
||||||
Str("method", r.Method).
|
Str("method", r.Method).
|
||||||
Str("url", r.URL.String()).
|
Str("url", r.URL.String()).
|
||||||
Str("remote", r.RemoteAddr).
|
Str("remote", r.RemoteAddr).
|
||||||
Msg("request")
|
Msgf("not found: %s", r.Host)
|
||||||
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
|
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
|
||||||
if ok {
|
if ok {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
@@ -100,39 +107,39 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Err(err).Msg("failed to write error page")
|
log.Err(err).Msg("failed to write error page")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func findRouteAnyDomain(host string) (types.HTTPRoute, error) {
|
func findRouteAnyDomain(host string) types.HTTPRoute {
|
||||||
idx := strings.IndexByte(host, '.')
|
idx := strings.IndexByte(host, '.')
|
||||||
if idx != -1 {
|
if idx != -1 {
|
||||||
target := host[:idx]
|
target := host[:idx]
|
||||||
if r, ok := routes.HTTP.Get(target); ok {
|
if r, ok := routes.HTTP.Get(target); ok {
|
||||||
return r, nil
|
return r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r, ok := routes.HTTP.Get(host); ok {
|
if r, ok := routes.HTTP.Get(host); ok {
|
||||||
return r, nil
|
return r
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, host)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findRouteByDomains(domains []string) func(host string) (types.HTTPRoute, error) {
|
func findRouteByDomains(domains []string) func(host string) types.HTTPRoute {
|
||||||
return func(host string) (types.HTTPRoute, error) {
|
return func(host string) types.HTTPRoute {
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
if target, ok := strings.CutSuffix(host, domain); ok {
|
if target, ok := strings.CutSuffix(host, domain); ok {
|
||||||
if r, ok := routes.HTTP.Get(target); ok {
|
if r, ok := routes.HTTP.Get(target); ok {
|
||||||
return r, nil
|
return r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback to exact match
|
// fallback to exact match
|
||||||
if r, ok := routes.HTTP.Get(host); ok {
|
if r, ok := routes.HTTP.Get(host); ok {
|
||||||
return r, nil
|
return r
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, host)
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,16 +29,15 @@ func run(t *testing.T, match []string, noMatch []string) {
|
|||||||
|
|
||||||
for _, test := range match {
|
for _, test := range match {
|
||||||
t.Run(test, func(t *testing.T) {
|
t.Run(test, func(t *testing.T) {
|
||||||
found, err := ep.findRouteFunc(test)
|
found := ep.findRouteFunc(test)
|
||||||
expect.NoError(t, err)
|
|
||||||
expect.NotNil(t, found)
|
expect.NotNil(t, found)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range noMatch {
|
for _, test := range noMatch {
|
||||||
t.Run(test, func(t *testing.T) {
|
t.Run(test, func(t *testing.T) {
|
||||||
_, err := ep.findRouteFunc(test)
|
found := ep.findRouteFunc(test)
|
||||||
expect.ErrorIs(t, ErrNoSuchRoute, err)
|
expect.Nil(t, found)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
internal/entrypoint/types/config.go
Normal file
16
internal/entrypoint/types/config.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||||
|
"github.com/yusing/godoxy/internal/route/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
SupportProxyProtocol bool `json:"support_proxy_protocol"`
|
||||||
|
Rules struct {
|
||||||
|
CatchAll rules.Rules `json:"catch_all"`
|
||||||
|
NotFound rules.Rules `json:"not_found"`
|
||||||
|
} `json:"rules"`
|
||||||
|
Middlewares []map[string]any `json:"middlewares"`
|
||||||
|
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
package idlewatcher
|
package idlewatcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
api "github.com/yusing/godoxy/internal/api/v1"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
httputils "github.com/yusing/goutils/http"
|
httputils "github.com/yusing/goutils/http"
|
||||||
"github.com/yusing/goutils/http/httpheaders"
|
"github.com/yusing/goutils/http/httpheaders"
|
||||||
|
|
||||||
|
_ "unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ForceCacheControl struct {
|
type ForceCacheControl struct {
|
||||||
@@ -44,6 +47,9 @@ func isFaviconPath(path string) bool {
|
|||||||
return path == "/favicon.ico"
|
return path == "/favicon.ico"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
|
||||||
|
func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResult, error)
|
||||||
|
|
||||||
func (w *Watcher) redirectToStartEndpoint(rw http.ResponseWriter, r *http.Request) {
|
func (w *Watcher) redirectToStartEndpoint(rw http.ResponseWriter, r *http.Request) {
|
||||||
uri := "/"
|
uri := "/"
|
||||||
if w.cfg.StartEndpoint != "" {
|
if w.cfg.StartEndpoint != "" {
|
||||||
@@ -62,7 +68,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
|
|||||||
|
|
||||||
// handle favicon request
|
// handle favicon request
|
||||||
if isFaviconPath(r.URL.Path) {
|
if isFaviconPath(r.URL.Path) {
|
||||||
result, err := api.GetFavIconFromAlias(r.Context(), w.route.Name())
|
result, err := GetFavIconFromAlias(r.Context(), w.route.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rw.WriteHeader(result.StatusCode)
|
rw.WriteHeader(result.StatusCode)
|
||||||
fmt.Fprint(rw, err)
|
fmt.Fprint(rw, err)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
config "github.com/yusing/godoxy/internal/config/types"
|
statequery "github.com/yusing/godoxy/internal/config/query"
|
||||||
"github.com/yusing/godoxy/internal/metrics/period"
|
"github.com/yusing/godoxy/internal/metrics/period"
|
||||||
metricsutils "github.com/yusing/godoxy/internal/metrics/utils"
|
metricsutils "github.com/yusing/godoxy/internal/metrics/utils"
|
||||||
"github.com/yusing/godoxy/internal/route/routes"
|
"github.com/yusing/godoxy/internal/route/routes"
|
||||||
@@ -133,7 +133,7 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
|
|||||||
r, ok := routes.Get(alias)
|
r, ok := routes.Get(alias)
|
||||||
if !ok {
|
if !ok {
|
||||||
// also search for excluded routes
|
// also search for excluded routes
|
||||||
r = config.GetInstance().SearchRoute(alias)
|
r = statequery.SearchRoute(alias)
|
||||||
}
|
}
|
||||||
if r != nil {
|
if r != nil {
|
||||||
displayName = r.DisplayName()
|
displayName = r.DisplayName()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type (
|
|||||||
FieldsBody []LogField
|
FieldsBody []LogField
|
||||||
ListBody []string
|
ListBody []string
|
||||||
MessageBody string
|
MessageBody string
|
||||||
ErrorBody struct {
|
errorBody struct {
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -40,6 +40,10 @@ func MakeLogFields(fields ...LogField) LogBody {
|
|||||||
return FieldsBody(fields)
|
return FieldsBody(fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ErrorBody(err error) LogBody {
|
||||||
|
return errorBody{Error: err}
|
||||||
|
}
|
||||||
|
|
||||||
func (f *LogFormat) Parse(format string) error {
|
func (f *LogFormat) Parse(format string) error {
|
||||||
switch format {
|
switch format {
|
||||||
case "":
|
case "":
|
||||||
@@ -116,7 +120,7 @@ func (m MessageBody) Format(format *LogFormat) ([]byte, error) {
|
|||||||
return m.Format(LogFormatMarkdown)
|
return m.Format(LogFormatMarkdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ErrorBody) Format(format *LogFormat) ([]byte, error) {
|
func (e errorBody) Format(format *LogFormat) ([]byte, error) {
|
||||||
switch format {
|
switch format {
|
||||||
case LogFormatRawJSON:
|
case LogFormatRawJSON:
|
||||||
return sonic.Marshal(e.Error)
|
return sonic.Marshal(e.Error)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/luthermonson/go-proxmox"
|
"github.com/luthermonson/go-proxmox"
|
||||||
"github.com/yusing/godoxy/internal/utils/pool"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -15,8 +14,6 @@ type Client struct {
|
|||||||
Version *proxmox.Version
|
Version *proxmox.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
var Clients = pool.New[*Client]("proxmox_clients")
|
|
||||||
|
|
||||||
func NewClient(baseUrl string, opts ...proxmox.Option) *Client {
|
func NewClient(baseUrl string, opts ...proxmox.Option) *Client {
|
||||||
return &Client{Client: proxmox.NewClient(baseUrl, opts...)}
|
return &Client{Client: proxmox.NewClient(baseUrl, opts...)}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import (
|
|||||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||||
"github.com/yusing/godoxy/internal/homepage"
|
"github.com/yusing/godoxy/internal/homepage"
|
||||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||||
|
provider "github.com/yusing/godoxy/internal/route/provider/types"
|
||||||
"github.com/yusing/godoxy/internal/utils/pool"
|
"github.com/yusing/godoxy/internal/utils/pool"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
"github.com/yusing/goutils/http/reverseproxy"
|
"github.com/yusing/goutils/http/reverseproxy"
|
||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
@@ -55,9 +57,15 @@ type (
|
|||||||
Stream() nettypes.Stream
|
Stream() nettypes.Stream
|
||||||
}
|
}
|
||||||
RouteProvider interface {
|
RouteProvider interface {
|
||||||
|
Start(task.Parent) gperr.Error
|
||||||
|
LoadRoutes() gperr.Error
|
||||||
GetRoute(alias string) (r Route, ok bool)
|
GetRoute(alias string) (r Route, ok bool)
|
||||||
IterRoutes(yield func(alias string, r Route) bool)
|
IterRoutes(yield func(alias string, r Route) bool)
|
||||||
|
NumRoutes() int
|
||||||
FindService(project, service string) (r Route, ok bool)
|
FindService(project, service string) (r Route, ok bool)
|
||||||
|
Statistics() ProviderStats
|
||||||
|
GetType() provider.Type
|
||||||
ShortName() string
|
ShortName() string
|
||||||
|
String() string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user