diff --git a/cmd/main.go b/cmd/main.go index d0f53046..f3b541c2 100755 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,6 +5,7 @@ import ( "sync" "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" @@ -17,6 +18,7 @@ import ( "github.com/yusing/godoxy/internal/net/gphttp/middleware" "github.com/yusing/godoxy/pkg" gperr "github.com/yusing/goutils/errs" + "github.com/yusing/goutils/server" "github.com/yusing/goutils/task" ) @@ -50,26 +52,26 @@ func main() { prepareDirectory(dir) } - cfg, err := config.Load() + err := config.Load() if err != nil { gperr.LogWarn("errors in config", err) } - cfg.Start(&config.StartServersOptions{ - Proxy: true, - }) + config.StartProxyServers() if err := auth.Initialize(); err != nil { log.Fatal().Err(err).Msg("failed to initialize authentication") } // API Handler needs to start after auth is initialized. - cfg.StartServers(&config.StartServersOptions{ - API: true, + server.StartServer(task.RootTask("api_server", false), server.Options{ + Name: "api", + HTTPAddr: common.APIHTTPAddr, + Handler: api.NewHandler(), }) uptime.Poller.Start() config.WatchChanges() - task.WaitExit(cfg.Value().TimeoutShutdown) + task.WaitExit(config.Value().TimeoutShutdown) } func prepareDirectory(dir string) { diff --git a/goutils b/goutils index 66b3d4cb..b2336ee8 160000 --- a/goutils +++ b/goutils @@ -1 +1 @@ -Subproject commit 66b3d4cbebd19b4b5d4c593c7e5e7b62c9d6c9eb +Subproject commit b2336ee8a6b2bca02f44c45c355659c12d7285b8 diff --git a/internal/api/v1/agent/verify.go b/internal/api/v1/agent/verify.go index b219ebc4..23473d66 100644 --- a/internal/api/v1/agent/verify.go +++ b/internal/api/v1/agent/verify.go @@ -9,7 +9,9 @@ import ( "github.com/yusing/godoxy/agent/pkg/agent" "github.com/yusing/godoxy/agent/pkg/certs" config "github.com/yusing/godoxy/internal/config/types" + "github.com/yusing/godoxy/internal/route/provider" apitypes "github.com/yusing/goutils/apitypes" + gperr "github.com/yusing/goutils/errs" ) type VerifyNewAgentRequest struct { @@ -57,7 +59,7 @@ func Verify(c *gin.Context) { return } - nRoutesAdded, err := config.GetInstance().VerifyNewAgent(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 @@ -76,3 +78,37 @@ func Verify(c *gin.Context) { 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 +} diff --git a/internal/api/v1/favicon.go b/internal/api/v1/favicon.go index ec431a81..b3e4c8af 100644 --- a/internal/api/v1/favicon.go +++ b/internal/api/v1/favicon.go @@ -8,6 +8,8 @@ import ( apitypes "github.com/yusing/godoxy/internal/api/types" "github.com/yusing/godoxy/internal/homepage" "github.com/yusing/godoxy/internal/route/routes" + + _ "unsafe" ) type GetFavIconRequest struct { @@ -62,6 +64,7 @@ func FavIcon(c *gin.Context) { c.Data(result.StatusCode, result.ContentType(), result.Icon) } +//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResult, error) { // try with route.Icon r, ok := routes.HTTP.Get(alias) diff --git a/internal/api/v1/reload.go b/internal/api/v1/reload.go index 7a37805d..b0e332c3 100644 --- a/internal/api/v1/reload.go +++ b/internal/api/v1/reload.go @@ -5,7 +5,7 @@ import ( "github.com/gin-gonic/gin" 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" @@ -20,7 +20,7 @@ import ( // @Failure 500 {object} apitypes.ErrorResponse // @Router /reload [post] 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")) return } diff --git a/internal/api/v1/route/providers.go b/internal/api/v1/route/providers.go index 2f265469..6a8600fa 100644 --- a/internal/api/v1/route/providers.go +++ b/internal/api/v1/route/providers.go @@ -5,7 +5,7 @@ import ( "time" "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/websocket" ) @@ -22,12 +22,11 @@ import ( // @Failure 500 {object} apitypes.ErrorResponse // @Router /route/providers [get] func Providers(c *gin.Context) { - cfg := config.GetInstance() if httpheaders.IsWebsocket(c.Request.Header) { websocket.PeriodicWrite(c, 5*time.Second, func() (any, error) { - return config.GetInstance().RouteProviderList(), nil + return statequery.RouteProviderList(), nil }) } else { - c.JSON(http.StatusOK, cfg.RouteProviderList()) + c.JSON(http.StatusOK, statequery.RouteProviderList()) } } diff --git a/internal/api/v1/route/route.go b/internal/api/v1/route/route.go index ca80d83f..7110fd16 100644 --- a/internal/api/v1/route/route.go +++ b/internal/api/v1/route/route.go @@ -5,7 +5,7 @@ import ( "github.com/gin-gonic/gin" 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" ) @@ -40,7 +40,7 @@ func Route(c *gin.Context) { } // also search for excluded routes - route = config.GetInstance().SearchRoute(request.Which) + route = statequery.SearchRoute(request.Which) if route != nil { c.JSON(http.StatusOK, route) return diff --git a/internal/api/v1/stats.go b/internal/api/v1/stats.go index 380d6c2c..aaa0573d 100644 --- a/internal/api/v1/stats.go +++ b/internal/api/v1/stats.go @@ -5,7 +5,7 @@ import ( "time" "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/goutils/http/httpheaders" "github.com/yusing/goutils/http/websocket" @@ -35,10 +35,9 @@ type ProxyStats struct { // @Failure 500 {object} apitypes.ErrorResponse // @Router /stats [get] func Stats(c *gin.Context) { - cfg := config.GetInstance() getStats := func() (any, error) { return map[string]any{ - "proxies": cfg.Statistics(), + "proxies": statequery.GetStatistics(), "uptime": int64(time.Since(startTime).Round(time.Second).Seconds()), }, nil } diff --git a/internal/config/agents.go b/internal/config/agents.go deleted file mode 100644 index 9bbaadb8..00000000 --- a/internal/config/agents.go +++ /dev/null @@ -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 -} diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index a787da85..00000000 --- a/internal/config/config.go +++ /dev/null @@ -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() -} diff --git a/internal/config/events.go b/internal/config/events.go new file mode 100644 index 00000000..6aa6e54f --- /dev/null +++ b/internal/config/events.go @@ -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, + }) +} diff --git a/internal/config/query.go b/internal/config/query.go deleted file mode 100644 index 0d319dfe..00000000 --- a/internal/config/query.go +++ /dev/null @@ -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, - } -} diff --git a/internal/config/query/query.go b/internal/config/query/query.go new file mode 100644 index 00000000..a8198616 --- /dev/null +++ b/internal/config/query/query.go @@ -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 +} diff --git a/internal/config/query/stats.go b/internal/config/query/stats.go new file mode 100644 index 00000000..6044b75a --- /dev/null +++ b/internal/config/query/stats.go @@ -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, + } +} diff --git a/internal/config/state.go b/internal/config/state.go new file mode 100644 index 00000000..437f8394 --- /dev/null +++ b/internal/config/state.go @@ -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() +} diff --git a/internal/config/types/config.go b/internal/config/types/config.go index adb9cc3c..d77ca41b 100644 --- a/internal/config/types/config.go +++ b/internal/config/types/config.go @@ -1,95 +1,47 @@ package config import ( - "context" "regexp" - "sync" + "sync/atomic" "github.com/go-playground/validator/v10" "github.com/yusing/godoxy/agent/pkg/agent" "github.com/yusing/godoxy/internal/acl" "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" "github.com/yusing/godoxy/internal/notif" "github.com/yusing/godoxy/internal/proxmox" "github.com/yusing/godoxy/internal/serialization" - "github.com/yusing/godoxy/internal/types" gperr "github.com/yusing/goutils/errs" ) type ( Config struct { - ACL *acl.Config `json:"acl"` - AutoCert *autocert.Config `json:"autocert"` - Entrypoint Entrypoint `json:"entrypoint"` - Providers Providers `json:"providers"` - MatchDomains []string `json:"match_domains" validate:"domain_name"` - Homepage HomepageConfig `json:"homepage"` - TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"` + ACL *acl.Config `json:"acl"` + AutoCert *autocert.Config `json:"autocert"` + Entrypoint entrypoint.Config `json:"entrypoint"` + Providers Providers `json:"providers"` + MatchDomains []string `json:"match_domains" validate:"domain_name"` + Homepage homepage.Config `json:"homepage"` + TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"` } Providers struct { 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"` - Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"` + Agents []agent.AgentConfig `json:"agents" yaml:"agents,omitempty"` Notification []notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"` Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,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 ( - instance ConfigInstance - instanceMu sync.RWMutex -) +// nil-safe +var ActiveConfig atomic.Pointer[Config] -func DefaultConfig() *Config { - return &Config{ - 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 init() { + ActiveConfig.Store(DefaultConfig()) } func Validate(data []byte) gperr.Error { @@ -97,6 +49,15 @@ func Validate(data []byte) gperr.Error { return serialization.UnmarshalValidateYAML(data, &model) } +func DefaultConfig() *Config { + return &Config{ + TimeoutShutdown: 3, + Homepage: homepage.Config{ + UseDefaultCategories: true, + }, + } +} + var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`) func init() { diff --git a/internal/config/types/state.go b/internal/config/types/state.go new file mode 100644 index 00000000..1aaa1c74 --- /dev/null +++ b/internal/config/types/state.go @@ -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] diff --git a/internal/entrypoint/entrypoint.go b/internal/entrypoint/entrypoint.go index e4bd7c54..989353e0 100644 --- a/internal/entrypoint/entrypoint.go +++ b/internal/entrypoint/entrypoint.go @@ -1,10 +1,9 @@ package entrypoint import ( - "errors" - "fmt" "net/http" "strings" + "sync/atomic" "github.com/rs/zerolog/log" "github.com/yusing/godoxy/internal/logging/accesslog" @@ -16,15 +15,21 @@ import ( ) type Entrypoint struct { - middleware *middleware.Middleware - accessLogger *accesslog.AccessLogger - findRouteFunc func(host string) (types.HTTPRoute, error) + middleware *middleware.Middleware + accessLogger *accesslog.AccessLogger + findRouteFunc func(host string) types.HTTPRoute } -var ErrNoSuchRoute = errors.New("no such route") +// nil-safe +var ActiveConfig atomic.Pointer[entrypoint.Config] -func NewEntrypoint() *Entrypoint { - return &Entrypoint{ +func init() { + // make sure it's not nil + ActiveConfig.Store(&entrypoint.Config{}) +} + +func NewEntrypoint() Entrypoint { + return Entrypoint{ findRouteFunc: findRouteAnyDomain, } } @@ -72,8 +77,10 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) { w = accesslog.NewResponseRecorder(w) 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) if ep.middleware != nil { 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. // 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.Err(err). + log.Error(). Str("method", r.Method). Str("url", r.URL.String()). Str("remote", r.RemoteAddr). - Msg("request") + Msgf("not found: %s", r.Host) errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound) if ok { 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") } } 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, '.') if idx != -1 { target := host[:idx] if r, ok := routes.HTTP.Get(target); ok { - return r, nil + return r } } 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) { - return func(host string) (types.HTTPRoute, error) { +func findRouteByDomains(domains []string) func(host string) types.HTTPRoute { + return func(host string) types.HTTPRoute { for _, domain := range domains { if target, ok := strings.CutSuffix(host, domain); ok { if r, ok := routes.HTTP.Get(target); ok { - return r, nil + return r } } } // fallback to exact match if r, ok := routes.HTTP.Get(host); ok { - return r, nil + return r } - return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, host) + return nil } } diff --git a/internal/entrypoint/entrypoint_test.go b/internal/entrypoint/entrypoint_test.go index 51a1b6b4..e5cd1bf2 100644 --- a/internal/entrypoint/entrypoint_test.go +++ b/internal/entrypoint/entrypoint_test.go @@ -29,16 +29,15 @@ func run(t *testing.T, match []string, noMatch []string) { for _, test := range match { t.Run(test, func(t *testing.T) { - found, err := ep.findRouteFunc(test) - expect.NoError(t, err) + found := ep.findRouteFunc(test) expect.NotNil(t, found) }) } for _, test := range noMatch { t.Run(test, func(t *testing.T) { - _, err := ep.findRouteFunc(test) - expect.ErrorIs(t, ErrNoSuchRoute, err) + found := ep.findRouteFunc(test) + expect.Nil(t, found) }) } } diff --git a/internal/entrypoint/types/config.go b/internal/entrypoint/types/config.go new file mode 100644 index 00000000..859a7cdb --- /dev/null +++ b/internal/entrypoint/types/config.go @@ -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"` +} diff --git a/internal/idlewatcher/handle_http.go b/internal/idlewatcher/handle_http.go index d956cf6a..b34ab5b0 100644 --- a/internal/idlewatcher/handle_http.go +++ b/internal/idlewatcher/handle_http.go @@ -1,13 +1,16 @@ package idlewatcher import ( + "context" "fmt" "net/http" "strconv" - api "github.com/yusing/godoxy/internal/api/v1" + "github.com/yusing/godoxy/internal/homepage" httputils "github.com/yusing/goutils/http" "github.com/yusing/goutils/http/httpheaders" + + _ "unsafe" ) type ForceCacheControl struct { @@ -44,6 +47,9 @@ func isFaviconPath(path string) bool { 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) { uri := "/" if w.cfg.StartEndpoint != "" { @@ -62,7 +68,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN // handle favicon request 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 { rw.WriteHeader(result.StatusCode) fmt.Fprint(rw, err) diff --git a/internal/metrics/uptime/uptime.go b/internal/metrics/uptime/uptime.go index faaaa79d..58845df8 100644 --- a/internal/metrics/uptime/uptime.go +++ b/internal/metrics/uptime/uptime.go @@ -8,7 +8,7 @@ import ( "github.com/bytedance/sonic" "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" metricsutils "github.com/yusing/godoxy/internal/metrics/utils" "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) if !ok { // also search for excluded routes - r = config.GetInstance().SearchRoute(alias) + r = statequery.SearchRoute(alias) } if r != nil { displayName = r.DisplayName() diff --git a/internal/notif/body.go b/internal/notif/body.go index 43f3900a..ad819b11 100644 --- a/internal/notif/body.go +++ b/internal/notif/body.go @@ -25,7 +25,7 @@ type ( FieldsBody []LogField ListBody []string MessageBody string - ErrorBody struct { + errorBody struct { Error error } ) @@ -40,6 +40,10 @@ func MakeLogFields(fields ...LogField) LogBody { return FieldsBody(fields) } +func ErrorBody(err error) LogBody { + return errorBody{Error: err} +} + func (f *LogFormat) Parse(format string) error { switch format { case "": @@ -116,7 +120,7 @@ func (m MessageBody) Format(format *LogFormat) ([]byte, error) { return m.Format(LogFormatMarkdown) } -func (e ErrorBody) Format(format *LogFormat) ([]byte, error) { +func (e errorBody) Format(format *LogFormat) ([]byte, error) { switch format { case LogFormatRawJSON: return sonic.Marshal(e.Error) diff --git a/internal/proxmox/client.go b/internal/proxmox/client.go index 8acdb809..49130d21 100644 --- a/internal/proxmox/client.go +++ b/internal/proxmox/client.go @@ -6,7 +6,6 @@ import ( "github.com/bytedance/sonic" "github.com/luthermonson/go-proxmox" - "github.com/yusing/godoxy/internal/utils/pool" ) type Client struct { @@ -15,8 +14,6 @@ type Client struct { Version *proxmox.Version } -var Clients = pool.New[*Client]("proxmox_clients") - func NewClient(baseUrl string, opts ...proxmox.Option) *Client { return &Client{Client: proxmox.NewClient(baseUrl, opts...)} } diff --git a/internal/types/routes.go b/internal/types/routes.go index 4feed2c7..37832f8e 100644 --- a/internal/types/routes.go +++ b/internal/types/routes.go @@ -6,7 +6,9 @@ import ( "github.com/yusing/godoxy/agent/pkg/agent" "github.com/yusing/godoxy/internal/homepage" nettypes "github.com/yusing/godoxy/internal/net/types" + provider "github.com/yusing/godoxy/internal/route/provider/types" "github.com/yusing/godoxy/internal/utils/pool" + gperr "github.com/yusing/goutils/errs" "github.com/yusing/goutils/http/reverseproxy" "github.com/yusing/goutils/task" ) @@ -55,9 +57,15 @@ type ( Stream() nettypes.Stream } RouteProvider interface { + Start(task.Parent) gperr.Error + LoadRoutes() gperr.Error GetRoute(alias string) (r Route, ok bool) IterRoutes(yield func(alias string, r Route) bool) + NumRoutes() int FindService(project, service string) (r Route, ok bool) + Statistics() ProviderStats + GetType() provider.Type ShortName() string + String() string } )