fix incorrect reload behaviors, further organize code

This commit is contained in:
yusing 2025-01-09 04:26:00 +08:00
parent 8bbb5d2e09
commit b3c47e759f
26 changed files with 418 additions and 336 deletions

View file

@ -23,10 +23,10 @@ lint:
enabled: enabled:
- hadolint@2.12.1-beta - hadolint@2.12.1-beta
- actionlint@1.7.6 - actionlint@1.7.6
- checkov@3.2.347 - checkov@3.2.350
- git-diff-check - git-diff-check
- gofmt@1.20.4 - gofmt@1.20.4
- golangci-lint@1.62.2 - golangci-lint@1.63.4
- osv-scanner@1.9.2 - osv-scanner@1.9.2
- oxipng@9.1.3 - oxipng@9.1.3
- prettier@3.4.2 - prettier@3.4.2

View file

@ -3,23 +3,19 @@ package main
import ( import (
"encoding/json" "encoding/json"
"log" "log"
"net/http"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time" "time"
"github.com/yusing/go-proxy/internal" "github.com/yusing/go-proxy/internal"
"github.com/yusing/go-proxy/internal/api"
"github.com/yusing/go-proxy/internal/api/v1/query" "github.com/yusing/go-proxy/internal/api/v1/query"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config" "github.com/yusing/go-proxy/internal/config"
"github.com/yusing/go-proxy/internal/entrypoint"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/metrics"
"github.com/yusing/go-proxy/internal/net/http/middleware" "github.com/yusing/go-proxy/internal/net/http/middleware"
"github.com/yusing/go-proxy/internal/net/http/server" "github.com/yusing/go-proxy/internal/route/routes"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg" "github.com/yusing/go-proxy/pkg"
) )
@ -97,16 +93,16 @@ func main() {
switch args.Command { switch args.Command {
case common.CommandListRoutes: case common.CommandListRoutes:
cfg.StartProxyProviders() cfg.StartProxyProviders()
printJSON(config.RoutesByAlias()) printJSON(routes.RoutesByAlias())
return return
case common.CommandListConfigs: case common.CommandListConfigs:
printJSON(config.Value()) printJSON(cfg.Value())
return return
case common.CommandDebugListEntries: case common.CommandDebugListEntries:
printJSON(config.DumpEntries()) printJSON(cfg.DumpEntries())
return return
case common.CommandDebugListProviders: case common.CommandDebugListProviders:
printJSON(config.DumpProviders()) printJSON(cfg.DumpProviders())
return return
} }
@ -114,7 +110,7 @@ func main() {
logging.Warn().Msg("API JWT secret is empty, authentication is disabled") logging.Warn().Msg("API JWT secret is empty, authentication is disabled")
} }
cfg.StartProxyProviders() cfg.Start()
config.WatchChanges() config.WatchChanges()
sig := make(chan os.Signal, 1) sig := make(chan os.Signal, 1)
@ -122,44 +118,12 @@ func main() {
signal.Notify(sig, syscall.SIGTERM) signal.Notify(sig, syscall.SIGTERM)
signal.Notify(sig, syscall.SIGHUP) signal.Notify(sig, syscall.SIGHUP)
autocert := config.GetAutoCertProvider()
if autocert != nil {
if err := autocert.Setup(); err != nil {
E.LogFatal("autocert setup error", err)
}
} else {
logging.Info().Msg("autocert not configured")
}
server.StartServer(server.Options{
Name: "proxy",
CertProvider: autocert,
HTTPAddr: common.ProxyHTTPAddr,
HTTPSAddr: common.ProxyHTTPSAddr,
Handler: http.HandlerFunc(entrypoint.Handler),
})
server.StartServer(server.Options{
Name: "api",
CertProvider: autocert,
HTTPAddr: common.APIHTTPAddr,
Handler: api.NewHandler(),
})
if common.PrometheusEnabled {
server.StartServer(server.Options{
Name: "metrics",
CertProvider: autocert,
HTTPAddr: common.MetricsHTTPAddr,
Handler: metrics.NewHandler(),
})
}
// wait for signal // wait for signal
<-sig <-sig
// grafully shutdown // grafully shutdown
logging.Info().Msg("shutting down") logging.Info().Msg("shutting down")
_ = task.GracefulShutdown(time.Second * time.Duration(config.Value().TimeoutShutdown)) _ = task.GracefulShutdown(time.Second * time.Duration(cfg.Value().TimeoutShutdown))
} }
func prepareDirectory(dir string) { func prepareDirectory(dir string) {

View file

@ -8,38 +8,41 @@ import (
"github.com/yusing/go-proxy/internal/api/v1/auth" "github.com/yusing/go-proxy/internal/api/v1/auth"
. "github.com/yusing/go-proxy/internal/api/v1/utils" . "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
config "github.com/yusing/go-proxy/internal/config/types"
) )
type ServeMux struct{ *http.ServeMux } type ServeMux struct{ *http.ServeMux }
func NewServeMux() ServeMux {
return ServeMux{http.NewServeMux()}
}
func (mux ServeMux) HandleFunc(method, endpoint string, handler http.HandlerFunc) { func (mux ServeMux) HandleFunc(method, endpoint string, handler http.HandlerFunc) {
mux.ServeMux.HandleFunc(method+" "+endpoint, checkHost(handler)) mux.ServeMux.HandleFunc(method+" "+endpoint, checkHost(handler))
} }
func NewHandler() http.Handler { func NewHandler(cfg config.ConfigInstance) http.Handler {
mux := NewServeMux() mux := ServeMux{http.NewServeMux()}
mux.HandleFunc("GET", "/v1", v1.Index) mux.HandleFunc("GET", "/v1", v1.Index)
mux.HandleFunc("GET", "/v1/version", v1.GetVersion) mux.HandleFunc("GET", "/v1/version", v1.GetVersion)
mux.HandleFunc("POST", "/v1/login", auth.LoginHandler) mux.HandleFunc("POST", "/v1/login", auth.LoginHandler)
mux.HandleFunc("GET", "/v1/logout", auth.LogoutHandler) mux.HandleFunc("GET", "/v1/logout", auth.LogoutHandler)
mux.HandleFunc("POST", "/v1/logout", auth.LogoutHandler) mux.HandleFunc("POST", "/v1/logout", auth.LogoutHandler)
mux.HandleFunc("POST", "/v1/reload", v1.Reload) mux.HandleFunc("POST", "/v1/reload", useCfg(cfg, v1.Reload))
mux.HandleFunc("GET", "/v1/list", auth.RequireAuth(v1.List)) mux.HandleFunc("GET", "/v1/list", auth.RequireAuth(useCfg(cfg, v1.List)))
mux.HandleFunc("GET", "/v1/list/{what}", auth.RequireAuth(v1.List)) mux.HandleFunc("GET", "/v1/list/{what}", auth.RequireAuth(useCfg(cfg, v1.List)))
mux.HandleFunc("GET", "/v1/list/{what}/{which}", auth.RequireAuth(v1.List)) mux.HandleFunc("GET", "/v1/list/{what}/{which}", auth.RequireAuth(useCfg(cfg, v1.List)))
mux.HandleFunc("GET", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.GetFileContent)) mux.HandleFunc("GET", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.GetFileContent))
mux.HandleFunc("POST", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.SetFileContent)) mux.HandleFunc("POST", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.SetFileContent))
mux.HandleFunc("PUT", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.SetFileContent)) mux.HandleFunc("PUT", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.SetFileContent))
mux.HandleFunc("GET", "/v1/schema/{filename...}", v1.GetSchemaFile) mux.HandleFunc("GET", "/v1/schema/{filename...}", v1.GetSchemaFile)
mux.HandleFunc("GET", "/v1/stats", v1.Stats) mux.HandleFunc("GET", "/v1/stats", useCfg(cfg, v1.Stats))
mux.HandleFunc("GET", "/v1/stats/ws", v1.StatsWS) mux.HandleFunc("GET", "/v1/stats/ws", useCfg(cfg, v1.StatsWS))
return mux return mux
} }
func useCfg(cfg config.ConfigInstance, handler func(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
handler(cfg, w, r)
}
}
// allow only requests to API server with localhost. // allow only requests to API server with localhost.
func checkHost(f http.HandlerFunc) http.HandlerFunc { func checkHost(f http.HandlerFunc) http.HandlerFunc {
if common.IsDebug { if common.IsDebug {
@ -55,4 +58,4 @@ func checkHost(f http.HandlerFunc) http.HandlerFunc {
LogDebug(r).Interface("headers", r.Header).Msg("API request") LogDebug(r).Interface("headers", r.Header).Msg("API request")
f(w, r) f(w, r)
} }
} }

View file

@ -9,7 +9,7 @@ import (
U "github.com/yusing/go-proxy/internal/api/v1/utils" U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config" config "github.com/yusing/go-proxy/internal/config/types"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/net/http/middleware" "github.com/yusing/go-proxy/internal/net/http/middleware"
"github.com/yusing/go-proxy/internal/route/provider" "github.com/yusing/go-proxy/internal/route/provider"

View file

@ -6,9 +6,10 @@ import (
U "github.com/yusing/go-proxy/internal/api/v1/utils" U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config" config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/http/middleware" "github.com/yusing/go-proxy/internal/net/http/middleware"
"github.com/yusing/go-proxy/internal/route" "github.com/yusing/go-proxy/internal/route/routes"
route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils"
) )
@ -24,7 +25,7 @@ const (
ListTasks = "tasks" ListTasks = "tasks"
) )
func List(w http.ResponseWriter, r *http.Request) { func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
what := r.PathValue("what") what := r.PathValue("what")
if what == "" { if what == "" {
what = ListRoutes what = ListRoutes
@ -40,7 +41,7 @@ func List(w http.ResponseWriter, r *http.Request) {
U.RespondJSON(w, r, route) U.RespondJSON(w, r, route)
} }
case ListRoutes: case ListRoutes:
U.RespondJSON(w, r, config.RoutesByAlias(route.RouteType(r.FormValue("type")))) U.RespondJSON(w, r, routes.RoutesByAlias(route.RouteType(r.FormValue("type"))))
case ListFiles: case ListFiles:
listFiles(w, r) listFiles(w, r)
case ListMiddlewares: case ListMiddlewares:
@ -48,9 +49,9 @@ func List(w http.ResponseWriter, r *http.Request) {
case ListMiddlewareTraces: case ListMiddlewareTraces:
U.RespondJSON(w, r, middleware.GetAllTrace()) U.RespondJSON(w, r, middleware.GetAllTrace())
case ListMatchDomains: case ListMatchDomains:
U.RespondJSON(w, r, config.Value().MatchDomains) U.RespondJSON(w, r, cfg.Value().MatchDomains)
case ListHomepageConfig: case ListHomepageConfig:
U.RespondJSON(w, r, config.HomepageConfig()) U.RespondJSON(w, r, routes.HomepageConfig(cfg.Value().Homepage.UseDefaultCategories))
case ListTasks: case ListTasks:
U.RespondJSON(w, r, task.DebugTaskList()) U.RespondJSON(w, r, task.DebugTaskList())
default: default:
@ -60,9 +61,9 @@ func List(w http.ResponseWriter, r *http.Request) {
func listRoute(which string) any { func listRoute(which string) any {
if which == "" || which == "all" { if which == "" || which == "all" {
return config.RoutesByAlias() return routes.RoutesByAlias()
} }
routes := config.RoutesByAlias() routes := routes.RoutesByAlias()
route, ok := routes[which] route, ok := routes[which]
if !ok { if !ok {
return nil return nil

View file

@ -4,11 +4,11 @@ import (
"net/http" "net/http"
U "github.com/yusing/go-proxy/internal/api/v1/utils" U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/config" config "github.com/yusing/go-proxy/internal/config/types"
) )
func Reload(w http.ResponseWriter, r *http.Request) { func Reload(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
if err := config.Reload(); err != nil { if err := cfg.Reload(); err != nil {
U.HandleErr(w, r, err) U.HandleErr(w, r, err)
return return
} }

View file

@ -9,25 +9,25 @@ import (
"github.com/coder/websocket/wsjson" "github.com/coder/websocket/wsjson"
U "github.com/yusing/go-proxy/internal/api/v1/utils" U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config" config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
) )
func Stats(w http.ResponseWriter, r *http.Request) { func Stats(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
U.RespondJSON(w, r, getStats()) U.RespondJSON(w, r, getStats(cfg))
} }
func StatsWS(w http.ResponseWriter, r *http.Request) { func StatsWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
var originPats []string var originPats []string
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"} localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
if len(config.Value().MatchDomains) == 0 { if len(cfg.Value().MatchDomains) == 0 {
U.LogWarn(r).Msg("no match domains configured, accepting websocket API request from all origins") U.LogWarn(r).Msg("no match domains configured, accepting websocket API request from all origins")
originPats = []string{"*"} originPats = []string{"*"}
} else { } else {
originPats = make([]string, len(config.Value().MatchDomains)) originPats = make([]string, len(cfg.Value().MatchDomains))
for i, domain := range config.Value().MatchDomains { for i, domain := range cfg.Value().MatchDomains {
originPats[i] = "*" + domain originPats[i] = "*" + domain
} }
originPats = append(originPats, localAddresses...) originPats = append(originPats, localAddresses...)
@ -52,7 +52,7 @@ func StatsWS(w http.ResponseWriter, r *http.Request) {
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
stats := getStats() stats := getStats(cfg)
if err := wsjson.Write(ctx, conn, stats); err != nil { if err := wsjson.Write(ctx, conn, stats); err != nil {
U.LogError(r).Msg("failed to write JSON") U.LogError(r).Msg("failed to write JSON")
return return
@ -62,9 +62,9 @@ func StatsWS(w http.ResponseWriter, r *http.Request) {
var startTime = time.Now() var startTime = time.Now()
func getStats() map[string]any { func getStats(cfg config.ConfigInstance) map[string]any {
return map[string]any{ return map[string]any{
"proxies": config.Statistics(), "proxies": cfg.Statistics(),
"uptime": strutils.FormatDuration(time.Since(startTime)), "uptime": strutils.FormatDuration(time.Since(startTime)),
} }
} }

View file

@ -18,8 +18,6 @@ func (p *Provider) Setup() (err E.Error) {
} }
} }
p.ScheduleRenewal()
for _, expiry := range p.GetExpiries() { for _, expiry := range p.GetExpiries() {
logger.Info().Msg("certificate expire on " + strutils.FormatTime(expiry)) logger.Info().Msg("certificate expire on " + strutils.FormatTime(expiry))
break break

View file

@ -7,12 +7,15 @@ import (
"sync" "sync"
"time" "time"
"github.com/yusing/go-proxy/internal/api"
"github.com/yusing/go-proxy/internal/autocert" "github.com/yusing/go-proxy/internal/autocert"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config/types" "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/entrypoint" "github.com/yusing/go-proxy/internal/entrypoint"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/metrics"
"github.com/yusing/go-proxy/internal/net/http/server"
"github.com/yusing/go-proxy/internal/notif" "github.com/yusing/go-proxy/internal/notif"
proxy "github.com/yusing/go-proxy/internal/route/provider" proxy "github.com/yusing/go-proxy/internal/route/provider"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
@ -26,7 +29,9 @@ type Config struct {
value *types.Config value *types.Config
providers F.Map[string, *proxy.Provider] providers F.Map[string, *proxy.Provider]
autocertProvider *autocert.Provider autocertProvider *autocert.Provider
task *task.Task entrypoint *entrypoint.Entrypoint
task *task.Task
} }
var ( var (
@ -45,15 +50,18 @@ Make sure you rename it back before next time you start.`
You may run "ls-config" to show or dump the current config.` You may run "ls-config" to show or dump the current config.`
) )
var Validate = types.Validate
func GetInstance() *Config { func GetInstance() *Config {
return instance return instance
} }
func newConfig() *Config { func newConfig() *Config {
return &Config{ return &Config{
value: types.DefaultConfig(), value: types.DefaultConfig(),
providers: F.NewMapOf[string, *proxy.Provider](), providers: F.NewMapOf[string, *proxy.Provider](),
task: task.RootTask("config", false), entrypoint: entrypoint.NewEntrypoint(),
task: task.RootTask("config", false),
} }
} }
@ -66,11 +74,6 @@ func Load() (*Config, E.Error) {
return instance, instance.load() return instance, instance.load()
} }
func Validate(data []byte) E.Error {
var model types.Config
return utils.DeserializeYAML(data, &model)
}
func MatchDomains() []string { func MatchDomains() []string {
return instance.value.MatchDomains return instance.value.MatchDomains
} }
@ -101,6 +104,7 @@ func OnConfigChange(ev []events.Event) {
} }
if err := Reload(); err != nil { if err := Reload(); err != nil {
logger.Warn().Msg("using last config")
// recovered in event queue // recovered in event queue
panic(err) panic(err)
} }
@ -122,15 +126,19 @@ func Reload() E.Error {
// -> replace config -> start new subtasks // -> replace config -> start new subtasks
instance.task.Finish("config changed") instance.task.Finish("config changed")
instance = newCfg instance = newCfg
instance.StartProxyProviders() instance.Start()
return nil return nil
} }
func Value() types.Config { func (cfg *Config) Value() *types.Config {
return *instance.value return instance.value
} }
func GetAutoCertProvider() *autocert.Provider { func (cfg *Config) Reload() E.Error {
return Reload()
}
func (cfg *Config) AutoCertProvider() *autocert.Provider {
return instance.autocertProvider return instance.autocertProvider
} }
@ -138,6 +146,26 @@ func (cfg *Config) Task() *task.Task {
return cfg.task return cfg.task
} }
func (cfg *Config) Start() {
cfg.StartAutoCert()
cfg.StartProxyProviders()
cfg.StartServers()
}
func (cfg *Config) StartAutoCert() {
autocert := cfg.autocertProvider
if autocert == nil {
logging.Info().Msg("autocert not configured")
return
}
if err := autocert.Setup(); err != nil {
E.LogFatal("autocert setup error", err)
} else {
autocert.ScheduleRenewal(cfg.task)
}
}
func (cfg *Config) StartProxyProviders() { func (cfg *Config) StartProxyProviders() {
errs := cfg.providers.CollectErrorsParallel( errs := cfg.providers.CollectErrorsParallel(
func(_ string, p *proxy.Provider) error { func(_ string, p *proxy.Provider) error {
@ -149,6 +177,30 @@ func (cfg *Config) StartProxyProviders() {
} }
} }
func (cfg *Config) StartServers() {
server.StartServer(cfg.task, server.Options{
Name: "proxy",
CertProvider: cfg.AutoCertProvider(),
HTTPAddr: common.ProxyHTTPAddr,
HTTPSAddr: common.ProxyHTTPSAddr,
Handler: cfg.entrypoint,
})
server.StartServer(cfg.task, server.Options{
Name: "api",
CertProvider: cfg.AutoCertProvider(),
HTTPAddr: common.APIHTTPAddr,
Handler: api.NewHandler(cfg),
})
if common.PrometheusEnabled {
server.StartServer(cfg.task, server.Options{
Name: "metrics",
CertProvider: cfg.AutoCertProvider(),
HTTPAddr: common.MetricsHTTPAddr,
Handler: metrics.NewHandler(),
})
}
}
func (cfg *Config) load() E.Error { func (cfg *Config) load() E.Error {
const errMsg = "config load error" const errMsg = "config load error"
@ -164,8 +216,8 @@ func (cfg *Config) load() E.Error {
// errors are non fatal below // errors are non fatal below
errs := E.NewBuilder(errMsg) errs := E.NewBuilder(errMsg)
errs.Add(entrypoint.SetMiddlewares(model.Entrypoint.Middlewares)) errs.Add(cfg.entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
errs.Add(entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog)) errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
errs.Add(cfg.initNotification(model.Providers.Notification)) errs.Add(cfg.initNotification(model.Providers.Notification))
errs.Add(cfg.initAutoCert(model.AutoCert)) errs.Add(cfg.initAutoCert(model.AutoCert))
errs.Add(cfg.loadRouteProviders(&model.Providers)) errs.Add(cfg.loadRouteProviders(&model.Providers))
@ -176,7 +228,8 @@ func (cfg *Config) load() E.Error {
model.MatchDomains[i] = "." + domain model.MatchDomains[i] = "." + domain
} }
} }
entrypoint.SetFindRouteDomains(model.MatchDomains) cfg.entrypoint.SetFindRouteDomains(model.MatchDomains)
return errs.Error() return errs.Error()
} }

View file

@ -1,20 +1,14 @@
package config package config
import ( import (
"strings"
"github.com/yusing/go-proxy/internal/homepage"
route "github.com/yusing/go-proxy/internal/route" route "github.com/yusing/go-proxy/internal/route"
"github.com/yusing/go-proxy/internal/route/entry" "github.com/yusing/go-proxy/internal/route/provider"
proxy "github.com/yusing/go-proxy/internal/route/provider"
"github.com/yusing/go-proxy/internal/route/routes"
"github.com/yusing/go-proxy/internal/route/types" "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/utils/strutils"
) )
func DumpEntries() map[string]*types.RawEntry { func (cfg *Config) DumpEntries() map[string]*types.RawEntry {
entries := make(map[string]*types.RawEntry) entries := make(map[string]*types.RawEntry)
instance.providers.RangeAll(func(_ string, p *proxy.Provider) { cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
p.RangeRoutes(func(alias string, r *route.Route) { p.RangeRoutes(func(alias string, r *route.Route) {
entries[alias] = r.Entry entries[alias] = r.Entry
}) })
@ -22,107 +16,20 @@ func DumpEntries() map[string]*types.RawEntry {
return entries return entries
} }
func DumpProviders() map[string]*proxy.Provider { func (cfg *Config) DumpProviders() map[string]*provider.Provider {
entries := make(map[string]*proxy.Provider) entries := make(map[string]*provider.Provider)
instance.providers.RangeAll(func(name string, p *proxy.Provider) { cfg.providers.RangeAll(func(name string, p *provider.Provider) {
entries[name] = p entries[name] = p
}) })
return entries return entries
} }
func HomepageConfig() homepage.Config { func (cfg *Config) Statistics() map[string]any {
hpCfg := homepage.NewHomePageConfig()
routes.GetHTTPRoutes().RangeAll(func(alias string, r types.HTTPRoute) {
en := r.RawEntry()
item := en.Homepage
if item == nil {
item = new(homepage.Item)
item.Show = true
}
if !item.IsEmpty() {
item.Show = true
}
if !item.Show {
return
}
item.Alias = alias
if item.Name == "" {
item.Name = strutils.Title(
strings.ReplaceAll(
strings.ReplaceAll(alias, "-", " "),
"_", " ",
),
)
}
if instance.value.Homepage.UseDefaultCategories {
if en.Container != nil && item.Category == "" {
if category, ok := homepage.PredefinedCategories[en.Container.ImageName]; ok {
item.Category = category
}
}
if item.Category == "" {
if category, ok := homepage.PredefinedCategories[strings.ToLower(alias)]; ok {
item.Category = category
}
}
}
switch {
case entry.IsDocker(r):
if item.Category == "" {
item.Category = "Docker"
}
item.SourceType = string(proxy.ProviderTypeDocker)
case entry.UseLoadBalance(r):
if item.Category == "" {
item.Category = "Load-balanced"
}
item.SourceType = "loadbalancer"
default:
if item.Category == "" {
item.Category = "Others"
}
item.SourceType = string(proxy.ProviderTypeFile)
}
item.AltURL = r.TargetURL().String()
hpCfg.Add(item)
})
return hpCfg
}
func RoutesByAlias(typeFilter ...route.RouteType) map[string]any {
rts := make(map[string]any)
if len(typeFilter) == 0 || typeFilter[0] == "" {
typeFilter = []route.RouteType{route.RouteTypeReverseProxy, route.RouteTypeStream}
}
for _, t := range typeFilter {
switch t {
case route.RouteTypeReverseProxy:
routes.GetHTTPRoutes().RangeAll(func(alias string, r types.HTTPRoute) {
rts[alias] = r
})
case route.RouteTypeStream:
routes.GetStreamRoutes().RangeAll(func(alias string, r types.StreamRoute) {
rts[alias] = r
})
}
}
return rts
}
func Statistics() map[string]any {
nTotalStreams := 0 nTotalStreams := 0
nTotalRPs := 0 nTotalRPs := 0
providerStats := make(map[string]proxy.ProviderStats) providerStats := make(map[string]provider.ProviderStats)
instance.providers.RangeAll(func(name string, p *proxy.Provider) { cfg.providers.RangeAll(func(name string, p *provider.Provider) {
stats := p.Statistics() stats := p.Statistics()
providerStats[name] = stats providerStats[name] = stats

View file

@ -3,6 +3,8 @@ package types
import ( import (
"github.com/yusing/go-proxy/internal/net/http/accesslog" "github.com/yusing/go-proxy/internal/net/http/accesslog"
"github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils"
E "github.com/yusing/go-proxy/internal/error"
) )
type ( type (
@ -24,6 +26,12 @@ type (
AccessLog *accesslog.Config `json:"access_log" validate:"omitempty"` AccessLog *accesslog.Config `json:"access_log" validate:"omitempty"`
} }
NotificationConfig map[string]any NotificationConfig map[string]any
ConfigInstance interface {
Value() *Config
Reload() E.Error
Statistics() map[string]any
}
) )
func DefaultConfig() *Config { func DefaultConfig() *Config {
@ -35,6 +43,11 @@ func DefaultConfig() *Config {
} }
} }
func Validate(data []byte) E.Error {
var model Config
return utils.DeserializeYAML(data, &model)
}
func init() { func init() {
utils.RegisterDefaultValueFactory(DefaultConfig) utils.RegisterDefaultValueFactory(DefaultConfig)
} }

View file

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"sync"
gphttp "github.com/yusing/go-proxy/internal/net/http" gphttp "github.com/yusing/go-proxy/internal/net/http"
"github.com/yusing/go-proxy/internal/net/http/accesslog" "github.com/yusing/go-proxy/internal/net/http/accesslog"
@ -17,32 +16,31 @@ import (
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
) )
var findRouteFunc = findRouteAnyDomain type Entrypoint struct {
middleware *middleware.Middleware
var ( accessLogger *accesslog.AccessLogger
epMiddleware *middleware.Middleware findRouteFunc func(host string) (route.HTTPRoute, error)
epMiddlewareMu sync.Mutex }
epAccessLogger *accesslog.AccessLogger
epAccessLoggerMu sync.Mutex
)
var ErrNoSuchRoute = errors.New("no such route") var ErrNoSuchRoute = errors.New("no such route")
func SetFindRouteDomains(domains []string) { func NewEntrypoint() *Entrypoint {
if len(domains) == 0 { return &Entrypoint{
findRouteFunc = findRouteAnyDomain findRouteFunc: findRouteAnyDomain,
} else {
findRouteFunc = findRouteByDomains(domains)
} }
} }
func SetMiddlewares(mws []map[string]any) error { func (ep *Entrypoint) SetFindRouteDomains(domains []string) {
epMiddlewareMu.Lock() if len(domains) == 0 {
defer epMiddlewareMu.Unlock() ep.findRouteFunc = findRouteAnyDomain
} else {
ep.findRouteFunc = findRouteByDomains(domains)
}
}
func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error {
if len(mws) == 0 { if len(mws) == 0 {
epMiddleware = nil ep.middleware = nil
return nil return nil
} }
@ -50,22 +48,19 @@ func SetMiddlewares(mws []map[string]any) error {
if err != nil { if err != nil {
return err return err
} }
epMiddleware = mid ep.middleware = mid
logger.Debug().Msg("entrypoint middleware loaded") logger.Debug().Msg("entrypoint middleware loaded")
return nil return nil
} }
func SetAccessLogger(parent task.Parent, cfg *accesslog.Config) (err error) { func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.Config) (err error) {
epAccessLoggerMu.Lock()
defer epAccessLoggerMu.Unlock()
if cfg == nil { if cfg == nil {
epAccessLogger = nil ep.accessLogger = nil
return return
} }
epAccessLogger, err = accesslog.NewFileAccessLogger(parent, cfg) ep.accessLogger, err = accesslog.NewFileAccessLogger(parent, cfg)
if err != nil { if err != nil {
return return
} }
@ -73,28 +68,18 @@ func SetAccessLogger(parent task.Parent, cfg *accesslog.Config) (err error) {
return return
} }
func Handler(w http.ResponseWriter, r *http.Request) { func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
mux, err := findRouteFunc(r.Host) mux, err := ep.findRouteFunc(r.Host)
if err == nil { if err == nil {
if epAccessLogger != nil { if ep.accessLogger != nil {
epMiddlewareMu.Lock() w = gphttp.NewModifyResponseWriter(w, r, func(resp *http.Response) error {
if epAccessLogger != nil { ep.accessLogger.Log(r, resp)
w = gphttp.NewModifyResponseWriter(w, r, func(resp *http.Response) error { return nil
epAccessLogger.Log(r, resp) })
return nil
})
}
epMiddlewareMu.Unlock()
} }
if epMiddleware != nil { if ep.middleware != nil {
epMiddlewareMu.Lock() ep.middleware.ServeHTTP(mux.ServeHTTP, w, r)
if epMiddleware != nil { return
mid := epMiddleware
epMiddlewareMu.Unlock()
mid.ServeHTTP(mux.ServeHTTP, w, r)
return
}
epMiddlewareMu.Unlock()
} }
mux.ServeHTTP(w, r) mux.ServeHTTP(w, r)
return return

View file

@ -8,18 +8,19 @@ import (
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"
) )
var r route.HTTPRoute var (
r route.HTTPRoute
ep = NewEntrypoint()
)
func run(t *testing.T, match []string, noMatch []string) { func run(t *testing.T, match []string, noMatch []string) {
t.Helper() t.Helper()
t.Cleanup(routes.TestClear) t.Cleanup(routes.TestClear)
t.Cleanup(func() { t.Cleanup(func() { ep.SetFindRouteDomains(nil) })
SetFindRouteDomains(nil)
})
for _, test := range match { for _, test := range match {
t.Run(test, func(t *testing.T) { t.Run(test, func(t *testing.T) {
found, err := findRouteFunc(test) found, err := ep.findRouteFunc(test)
ExpectNoError(t, err) ExpectNoError(t, err)
ExpectTrue(t, found == &r) ExpectTrue(t, found == &r)
}) })
@ -27,7 +28,7 @@ func run(t *testing.T, match []string, noMatch []string) {
for _, test := range noMatch { for _, test := range noMatch {
t.Run(test, func(t *testing.T) { t.Run(test, func(t *testing.T) {
_, err := findRouteFunc(test) _, err := ep.findRouteFunc(test)
ExpectError(t, ErrNoSuchRoute, err) ExpectError(t, ErrNoSuchRoute, err)
}) })
} }
@ -72,7 +73,7 @@ func TestFindRouteExactHostMatch(t *testing.T) {
} }
func TestFindRouteByDomains(t *testing.T) { func TestFindRouteByDomains(t *testing.T) {
SetFindRouteDomains([]string{ ep.SetFindRouteDomains([]string{
".domain.com", ".domain.com",
".sub.domain.com", ".sub.domain.com",
}) })
@ -97,7 +98,7 @@ func TestFindRouteByDomains(t *testing.T) {
} }
func TestFindRouteByDomainsExactMatch(t *testing.T) { func TestFindRouteByDomainsExactMatch(t *testing.T) {
SetFindRouteDomains([]string{ ep.SetFindRouteDomains([]string{
".domain.com", ".domain.com",
".sub.domain.com", ".sub.domain.com",
}) })

View file

@ -8,8 +8,8 @@ import (
//nolint:errname //nolint:errname
type withSubject struct { type withSubject struct {
Subject string `json:"subject"` Subjects []string `json:"subjects"`
Err error `json:"err"` Err error `json:"err"`
} }
const subjectSep = " > " const subjectSep = " > "
@ -30,13 +30,18 @@ func PrependSubject(subject string, err error) error {
case Error: case Error:
return err.Subject(subject) return err.Subject(subject)
} }
return &withSubject{subject, err} return &withSubject{[]string{subject}, err}
} }
func (err *withSubject) Prepend(subject string) *withSubject { func (err *withSubject) Prepend(subject string) *withSubject {
clone := *err clone := *err
if subject != "" { if subject != "" {
clone.Subject = subject + subjectSep + clone.Subject switch subject[0] {
case '[', '(', '{':
clone.Subjects[len(clone.Subjects)-1] += subject
default:
clone.Subjects = append(clone.Subjects, subject)
}
} }
return &clone return &clone
} }
@ -50,7 +55,22 @@ func (err *withSubject) Unwrap() error {
} }
func (err *withSubject) Error() string { func (err *withSubject) Error() string {
subjects := strings.Split(err.Subject, subjectSep) // subject is in reversed order
subjects[len(subjects)-1] = highlight(subjects[len(subjects)-1]) n := len(err.Subjects)
return strings.Join(subjects, subjectSep) + ": " + err.Err.Error() size := 0
errStr := err.Err.Error()
var sb strings.Builder
for _, s := range err.Subjects {
size += len(s)
}
sb.Grow(size + 2 + n*len(subjectSep) + len(errStr))
for i := n - 1; i > 0; i-- {
sb.WriteString(err.Subjects[i])
sb.WriteString(subjectSep)
}
sb.WriteString(highlight(err.Subjects[0]))
sb.WriteString(": ")
sb.WriteString(errStr)
return sb.String()
} }

View file

@ -4,7 +4,10 @@ import (
"net" "net"
"net/http" "net/http"
"github.com/go-playground/validator/v10"
gphttp "github.com/yusing/go-proxy/internal/net/http"
"github.com/yusing/go-proxy/internal/net/types" "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/utils"
F "github.com/yusing/go-proxy/internal/utils/functional" F "github.com/yusing/go-proxy/internal/utils/functional"
) )
@ -16,7 +19,7 @@ type (
} }
CIDRWhitelistOpts struct { CIDRWhitelistOpts struct {
Allow []*types.CIDR `validate:"min=1"` Allow []*types.CIDR `validate:"min=1"`
StatusCode int `json:"status_code" aliases:"status" validate:"omitempty,gte=400,lte=599"` StatusCode int `json:"status_code" aliases:"status" validate:"omitempty,status_code"`
Message string Message string
} }
) )
@ -30,6 +33,13 @@ var (
} }
) )
func init() {
utils.Validator().RegisterValidation("status_code", func(fl validator.FieldLevel) bool {
statusCode := fl.Field().Int()
return gphttp.IsStatusCodeValid(int(statusCode))
})
}
// setup implements MiddlewareWithSetup. // setup implements MiddlewareWithSetup.
func (wl *cidrWhitelist) setup() { func (wl *cidrWhitelist) setup() {
wl.CIDRWhitelistOpts = cidrWhitelistDefaults wl.CIDRWhitelistOpts = cidrWhitelistDefaults

View file

@ -24,6 +24,18 @@ func TestCIDRWhitelistValidation(t *testing.T) {
"message": testMessage, "message": testMessage,
}) })
ExpectNoError(t, err) ExpectNoError(t, err)
_, err = CIDRWhiteList.New(OptionsRaw{
"allow": []string{"192.168.2.100/32"},
"message": testMessage,
"status": 403,
})
ExpectNoError(t, err)
_, err = CIDRWhiteList.New(OptionsRaw{
"allow": []string{"192.168.2.100/32"},
"message": testMessage,
"status_code": 403,
})
ExpectNoError(t, err)
}) })
t.Run("missing allow", func(t *testing.T) { t.Run("missing allow", func(t *testing.T) {
_, err := CIDRWhiteList.New(OptionsRaw{ _, err := CIDRWhiteList.New(OptionsRaw{

View file

@ -168,24 +168,6 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
// URLs to the scheme, host, and base path provided in target. If the // URLs to the scheme, host, and base path provided in target. If the
// target's path is "/base" and the incoming request was for "/dir", // target's path is "/base" and the incoming request was for "/dir",
// the target request will be for /base/dir. // the target request will be for /base/dir.
//
// NewReverseProxy does not rewrite the Host header.
//
// To customize the ReverseProxy behavior beyond what
// NewReverseProxy provides, use ReverseProxy directly
// with a Rewrite function. The ProxyRequest SetURL method
// may be used to route the outbound request. (Note that SetURL,
// unlike NewReverseProxy, rewrites the Host header
// of the outbound request by default.)
//
// proxy := &ReverseProxy{
// Rewrite: func(r *ProxyRequest) {
// r.SetURL(target)
// r.Out.Host = r.In.Host // if desired
// },
// }
//
func NewReverseProxy(name string, target types.URL, transport http.RoundTripper) *ReverseProxy { func NewReverseProxy(name string, target types.URL, transport http.RoundTripper) *ReverseProxy {
if transport == nil { if transport == nil {
panic("nil transport") panic("nil transport")

View file

@ -35,9 +35,9 @@ type Options struct {
Handler http.Handler Handler http.Handler
} }
func StartServer(opt Options) (s *Server) { func StartServer(parent task.Parent, opt Options) (s *Server) {
s = NewServer(opt) s = NewServer(opt)
s.Start() s.Start(parent)
return s return s
} }
@ -83,11 +83,13 @@ func NewServer(opt Options) (s *Server) {
// If both are not set, this does nothing. // If both are not set, this does nothing.
// //
// Start() is non-blocking. // Start() is non-blocking.
func (s *Server) Start() { func (s *Server) Start(parent task.Parent) {
if s.http == nil && s.https == nil { if s.http == nil && s.https == nil {
return return
} }
task := parent.Subtask("server."+s.Name, false)
s.startTime = time.Now() s.startTime = time.Now()
if s.http != nil { if s.http != nil {
go func() { go func() {
@ -105,7 +107,7 @@ func (s *Server) Start() {
s.l.Info().Str("addr", s.https.Addr).Msgf("server started") s.l.Info().Str("addr", s.https.Addr).Msgf("server started")
} }
task.OnProgramExit("server."+s.Name+".stop", s.stop) task.OnCancel("stop", s.stop)
} }
func (s *Server) stop() { func (s *Server) stop() {
@ -113,14 +115,19 @@ func (s *Server) stop() {
return return
} }
ctx, cancel := context.WithTimeout(task.RootContext(), 3*time.Second)
defer cancel()
if s.http != nil && s.httpStarted { if s.http != nil && s.httpStarted {
s.handleErr("http", s.http.Shutdown(task.RootContext())) s.handleErr("http", s.http.Shutdown(ctx))
s.httpStarted = false s.httpStarted = false
s.l.Info().Str("addr", s.http.Addr).Msgf("server stopped")
} }
if s.https != nil && s.httpsStarted { if s.https != nil && s.httpsStarted {
s.handleErr("https", s.https.Shutdown(task.RootContext())) s.handleErr("https", s.https.Shutdown(ctx))
s.httpsStarted = false s.httpsStarted = false
s.l.Info().Str("addr", s.https.Addr).Msgf("server stopped")
} }
} }

View file

@ -72,9 +72,9 @@ proxy.app1.host: 10.0.0.254
proxy.app1.port: 80 proxy.app1.port: 80
proxy.app1.path_patterns: proxy.app1.path_patterns:
| # Check https://pkg.go.dev/net/http#hdr-Patterns-ServeMux for syntax | # Check https://pkg.go.dev/net/http#hdr-Patterns-ServeMux for syntax
GET / # accept any GET request - GET / # accept any GET request
POST /auth # for /auth and /auth/* accept only POST - POST /auth # for /auth and /auth/* accept only POST
GET /home/{$} # for exactly /home - GET /home/{$} # for exactly /home
proxy.app1.healthcheck.disabled: false proxy.app1.healthcheck.disabled: false
proxy.app1.healthcheck.path: / proxy.app1.healthcheck.path: /
proxy.app1.healthcheck.interval: 5s proxy.app1.healthcheck.interval: 5s

View file

@ -5,6 +5,7 @@ import (
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/route" "github.com/yusing/go-proxy/internal/route"
"github.com/yusing/go-proxy/internal/route/entry" "github.com/yusing/go-proxy/internal/route/entry"
"github.com/yusing/go-proxy/internal/route/provider/types"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/watcher" "github.com/yusing/go-proxy/internal/watcher"
) )
@ -87,10 +88,10 @@ func (handler *EventHandler) matchAny(events []watcher.Event, route *route.Route
func (handler *EventHandler) match(event watcher.Event, route *route.Route) bool { func (handler *EventHandler) match(event watcher.Event, route *route.Route) bool {
switch handler.provider.GetType() { switch handler.provider.GetType() {
case ProviderTypeDocker: case types.ProviderTypeDocker:
return route.Entry.Container.ContainerID == event.ActorID || return route.Entry.Container.ContainerID == event.ActorID ||
route.Entry.Container.ContainerName == event.ActorName route.Entry.Container.ContainerName == event.ActorName
case ProviderTypeFile: case types.ProviderTypeFile:
return true return true
} }
// should never happen // should never happen

View file

@ -10,6 +10,8 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
R "github.com/yusing/go-proxy/internal/route" R "github.com/yusing/go-proxy/internal/route"
"github.com/yusing/go-proxy/internal/route/provider/types"
route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
W "github.com/yusing/go-proxy/internal/watcher" W "github.com/yusing/go-proxy/internal/watcher"
"github.com/yusing/go-proxy/internal/watcher/events" "github.com/yusing/go-proxy/internal/watcher/events"
@ -20,7 +22,7 @@ type (
ProviderImpl `json:"-"` ProviderImpl `json:"-"`
name string name string
t ProviderType t types.ProviderType
routes R.Routes routes R.Routes
watcher W.Watcher watcher W.Watcher
@ -31,24 +33,20 @@ type (
NewWatcher() W.Watcher NewWatcher() W.Watcher
Logger() *zerolog.Logger Logger() *zerolog.Logger
} }
ProviderType string
ProviderStats struct { ProviderStats struct {
NumRPs int `json:"num_reverse_proxies"` NumRPs int `json:"num_reverse_proxies"`
NumStreams int `json:"num_streams"` NumStreams int `json:"num_streams"`
Type ProviderType `json:"type"` Type types.ProviderType `json:"type"`
} }
) )
const ( const (
ProviderTypeDocker ProviderType = "docker"
ProviderTypeFile ProviderType = "file"
providerEventFlushInterval = 300 * time.Millisecond providerEventFlushInterval = 300 * time.Millisecond
) )
var ErrEmptyProviderName = errors.New("empty provider name") var ErrEmptyProviderName = errors.New("empty provider name")
func newProvider(name string, t ProviderType) *Provider { func newProvider(name string, t types.ProviderType) *Provider {
return &Provider{ return &Provider{
name: name, name: name,
t: t, t: t,
@ -61,7 +59,7 @@ func NewFileProvider(filename string) (p *Provider, err error) {
if name == "" { if name == "" {
return nil, ErrEmptyProviderName return nil, ErrEmptyProviderName
} }
p = newProvider(strings.ReplaceAll(name, ".", "_"), ProviderTypeFile) p = newProvider(strings.ReplaceAll(name, ".", "_"), types.ProviderTypeFile)
p.ProviderImpl, err = FileProviderImpl(filename) p.ProviderImpl, err = FileProviderImpl(filename)
if err != nil { if err != nil {
return nil, err return nil, err
@ -75,7 +73,7 @@ func NewDockerProvider(name string, dockerHost string) (p *Provider, err error)
return nil, ErrEmptyProviderName return nil, ErrEmptyProviderName
} }
p = newProvider(name, ProviderTypeDocker) p = newProvider(name, types.ProviderTypeDocker)
p.ProviderImpl, err = DockerProviderImpl(name, dockerHost, p.IsExplicitOnly()) p.ProviderImpl, err = DockerProviderImpl(name, dockerHost, p.IsExplicitOnly())
if err != nil { if err != nil {
return nil, err return nil, err
@ -92,7 +90,7 @@ func (p *Provider) GetName() string {
return p.name return p.name
} }
func (p *Provider) GetType() ProviderType { func (p *Provider) GetType() types.ProviderType {
return p.t return p.t
} }
@ -171,9 +169,9 @@ func (p *Provider) Statistics() ProviderStats {
numStreams := 0 numStreams := 0
p.routes.RangeAll(func(_ string, r *R.Route) { p.routes.RangeAll(func(_ string, r *R.Route) {
switch r.Type { switch r.Type {
case R.RouteTypeReverseProxy: case route.RouteTypeReverseProxy:
numRPs++ numRPs++
case R.RouteTypeStream: case route.RouteTypeStream:
numStreams++ numStreams++
} }
}) })

View file

@ -0,0 +1,8 @@
package types
type ProviderType string
const (
ProviderTypeDocker ProviderType = "docker"
ProviderTypeFile ProviderType = "file"
)

View file

@ -14,11 +14,10 @@ import (
) )
type ( type (
RouteType string Route struct {
Route struct {
_ U.NoCopy _ U.NoCopy
impl impl
Type RouteType Type types.RouteType
Entry *RawEntry Entry *RawEntry
} }
Routes = F.Map[string, *Route] Routes = F.Map[string, *Route]
@ -34,11 +33,6 @@ type (
RawEntries = types.RawEntries RawEntries = types.RawEntries
) )
const (
RouteTypeStream RouteType = "stream"
RouteTypeReverseProxy RouteType = "reverse_proxy"
)
// function alias. // function alias.
var ( var (
NewRoutes = F.NewMap[Routes] NewRoutes = F.NewMap[Routes]
@ -59,15 +53,15 @@ func NewRoute(raw *RawEntry) (*Route, E.Error) {
return nil, err return nil, err
} }
var t RouteType var t types.RouteType
var rt impl var rt impl
switch e := en.(type) { switch e := en.(type) {
case *entry.StreamEntry: case *entry.StreamEntry:
t = RouteTypeStream t = types.RouteTypeStream
rt, err = NewStreamRoute(e) rt, err = NewStreamRoute(e)
case *entry.ReverseProxyEntry: case *entry.ReverseProxyEntry:
t = RouteTypeReverseProxy t = types.RouteTypeReverseProxy
rt, err = NewHTTPRoute(e) rt, err = NewHTTPRoute(e)
default: default:
panic("bug: should not reach here") panic("bug: should not reach here")

View file

@ -0,0 +1,99 @@
package routes
import (
"strings"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/route/entry"
provider "github.com/yusing/go-proxy/internal/route/provider/types"
"github.com/yusing/go-proxy/internal/route/types"
route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
func HomepageConfig(useDefaultCategories bool) homepage.Config {
hpCfg := homepage.NewHomePageConfig()
GetHTTPRoutes().RangeAll(func(alias string, r types.HTTPRoute) {
en := r.RawEntry()
item := en.Homepage
if item == nil {
item = new(homepage.Item)
item.Show = true
}
if !item.IsEmpty() {
item.Show = true
}
if !item.Show {
return
}
item.Alias = alias
if item.Name == "" {
item.Name = strutils.Title(
strings.ReplaceAll(
strings.ReplaceAll(alias, "-", " "),
"_", " ",
),
)
}
if useDefaultCategories {
if en.Container != nil && item.Category == "" {
if category, ok := homepage.PredefinedCategories[en.Container.ImageName]; ok {
item.Category = category
}
}
if item.Category == "" {
if category, ok := homepage.PredefinedCategories[strings.ToLower(alias)]; ok {
item.Category = category
}
}
}
switch {
case entry.IsDocker(r):
if item.Category == "" {
item.Category = "Docker"
}
item.SourceType = string(provider.ProviderTypeDocker)
case entry.UseLoadBalance(r):
if item.Category == "" {
item.Category = "Load-balanced"
}
item.SourceType = "loadbalancer"
default:
if item.Category == "" {
item.Category = "Others"
}
item.SourceType = string(provider.ProviderTypeFile)
}
item.AltURL = r.TargetURL().String()
hpCfg.Add(item)
})
return hpCfg
}
func RoutesByAlias(typeFilter ...route.RouteType) map[string]any {
rts := make(map[string]any)
if len(typeFilter) == 0 || typeFilter[0] == "" {
typeFilter = []route.RouteType{route.RouteTypeReverseProxy, route.RouteTypeStream}
}
for _, t := range typeFilter {
switch t {
case route.RouteTypeReverseProxy:
GetHTTPRoutes().RangeAll(func(alias string, r types.HTTPRoute) {
rts[alias] = r
})
case route.RouteTypeStream:
GetStreamRoutes().RangeAll(func(alias string, r types.StreamRoute) {
rts[alias] = r
})
}
}
return rts
}

View file

@ -0,0 +1,8 @@
package types
type RouteType string
const (
RouteTypeStream RouteType = "stream"
RouteTypeReverseProxy RouteType = "reverse_proxy"
)

View file

@ -8,21 +8,21 @@ GoDoxy v0.8.2 expected changes
Sample service showing this: Sample service showing this:
```yaml ```yaml
hello-world: hello-world:
image: nginxdemos/hello image: nginxdemos/hello
container_name: hello-world container_name: hello-world
restart: "no" restart: "no"
ports: ports:
- "9100:80" - "9100:80"
labels: labels:
proxy.aliases: hello-world proxy.aliases: hello-world
proxy.#1.port: 9100 proxy.#1.port: 9100
proxy.idle_timeout: 45s proxy.idle_timeout: 45s
proxy.wake_timeout: 30s proxy.wake_timeout: 30s
proxy.stop_method: stop proxy.stop_method: stop
proxy.stop_timeout: 10s proxy.stop_timeout: 10s
proxy.stop_signal: SIGTERM proxy.stop_signal: SIGTERM
proxy.start_endpoint: "/start" proxy.start_endpoint: "/start"
``` ```
Hitting `/` on this service when the container is down: Hitting `/` on this service when the container is down:
@ -38,14 +38,14 @@ GoDoxy v0.8.2 expected changes
> Host: hello-world.godoxy.local > Host: hello-world.godoxy.local
> User-Agent: curl/8.7.1 > User-Agent: curl/8.7.1
> Accept: */* > Accept: */*
> >
* Request completely sent off * Request completely sent off
< HTTP/1.1 403 Forbidden < HTTP/1.1 403 Forbidden
< Content-Type: text/plain; charset=utf-8 < Content-Type: text/plain; charset=utf-8
< X-Content-Type-Options: nosniff < X-Content-Type-Options: nosniff
< Date: Wed, 08 Jan 2025 02:04:51 GMT < Date: Wed, 08 Jan 2025 02:04:51 GMT
< Content-Length: 71 < Content-Length: 71
< <
Forbidden: Container can only be started via configured start endpoint Forbidden: Container can only be started via configured start endpoint
* Connection #0 to host localhost left intact * Connection #0 to host localhost left intact
``` ```
@ -64,16 +64,17 @@ GoDoxy v0.8.2 expected changes
> User-Agent: curl/8.7.1 > User-Agent: curl/8.7.1
> Accept: */* > Accept: */*
> X-Goproxy-Check-Redirect: skip > X-Goproxy-Check-Redirect: skip
> >
* Request completely sent off * Request completely sent off
< HTTP/1.1 200 OK < HTTP/1.1 200 OK
< Date: Wed, 08 Jan 2025 02:13:39 GMT < Date: Wed, 08 Jan 2025 02:13:39 GMT
< Content-Length: 0 < Content-Length: 0
< <
* Connection #0 to host localhost left intact * Connection #0 to host localhost left intact
``` ```
- Caddyfile like rules - Caddyfile like rules
```yaml ```yaml
proxy.goaccess.rules: | proxy.goaccess.rules: |
- name: default - name: default
@ -92,4 +93,21 @@ GoDoxy v0.8.2 expected changes
- name: block POST and PUT - name: block POST and PUT
on: method POST | method PUT on: method POST | method PUT
do: error 403 Forbidden do: error 403 Forbidden
``` ```
````
- config reload will now cause all servers to fully restart (i.e. proxy, api, prometheus, etc)
- multiline-string as list now treated as YAML list, which requires hyphen prefix `-`, i.e.
```yaml
proxy.app.middlewares.request.hide_headers:
- X-Header1
- X-Header2
````
- autocert now supports hot-reload
- Fixes
- bug: cert renewal failure no longer causes renew schdueler to stuck forever
- bug: access log writes to closed file after config reload