diff --git a/cmd/main.go b/cmd/main.go index ffa8f199..64e10a2f 100755 --- a/cmd/main.go +++ b/cmd/main.go @@ -14,12 +14,12 @@ import ( "github.com/yusing/go-proxy/internal/api/v1/query" "github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/config" + "github.com/yusing/go-proxy/internal/entrypoint" E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/metrics" "github.com/yusing/go-proxy/internal/net/http/middleware" - R "github.com/yusing/go-proxy/internal/route" - "github.com/yusing/go-proxy/internal/server" + "github.com/yusing/go-proxy/internal/net/http/server" "github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/pkg" ) @@ -136,7 +136,7 @@ func main() { CertProvider: autocert, HTTPAddr: common.ProxyHTTPAddr, HTTPSAddr: common.ProxyHTTPSAddr, - Handler: http.HandlerFunc(R.ProxyHandler), + Handler: http.HandlerFunc(entrypoint.Handler), RedirectToHTTPS: config.Value().RedirectToHTTPS, }) server.StartServer(server.Options{ diff --git a/internal/config/config.go b/internal/config/config.go index 3ef0bd0c..770045ad 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,10 +10,10 @@ import ( "github.com/yusing/go-proxy/internal/autocert" "github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/config/types" + "github.com/yusing/go-proxy/internal/entrypoint" E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/notif" - "github.com/yusing/go-proxy/internal/route" proxy "github.com/yusing/go-proxy/internal/route/provider" "github.com/yusing/go-proxy/internal/task" U "github.com/yusing/go-proxy/internal/utils" @@ -183,7 +183,7 @@ func (cfg *Config) load() E.Error { model.MatchDomains[i] = "." + domain } } - route.SetFindMuxDomains(model.MatchDomains) + entrypoint.SetFindRouteDomains(model.MatchDomains) return errs.Error() } diff --git a/internal/config/query.go b/internal/config/query.go index 76877ebb..07308498 100644 --- a/internal/config/query.go +++ b/internal/config/query.go @@ -6,14 +6,16 @@ import ( "github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/homepage" - "github.com/yusing/go-proxy/internal/proxy/entry" - "github.com/yusing/go-proxy/internal/route" + route "github.com/yusing/go-proxy/internal/route" + "github.com/yusing/go-proxy/internal/route/entry" 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/utils/strutils" ) -func DumpEntries() map[string]*entry.RawEntry { - entries := make(map[string]*entry.RawEntry) +func DumpEntries() map[string]*types.RawEntry { + entries := make(map[string]*types.RawEntry) instance.providers.RangeAll(func(_ string, p *proxy.Provider) { p.RangeRoutes(func(alias string, r *route.Route) { entries[alias] = r.Entry @@ -43,8 +45,8 @@ func HomepageConfig() homepage.Config { } hpCfg := homepage.NewHomePageConfig() - route.GetReverseProxies().RangeAll(func(alias string, r *route.HTTPRoute) { - en := r.Raw + routes.GetHTTPRoutes().RangeAll(func(alias string, r types.HTTPRoute) { + en := r.RawEntry() item := en.Homepage if item == nil { item = new(homepage.Item) @@ -113,23 +115,23 @@ func HomepageConfig() homepage.Config { } func RoutesByAlias(typeFilter ...route.RouteType) map[string]any { - routes := make(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: - route.GetReverseProxies().RangeAll(func(alias string, r *route.HTTPRoute) { - routes[alias] = r + routes.GetHTTPRoutes().RangeAll(func(alias string, r types.HTTPRoute) { + rts[alias] = r }) case route.RouteTypeStream: - route.GetStreamProxies().RangeAll(func(alias string, r *route.StreamRoute) { - routes[alias] = r + routes.GetStreamRoutes().RangeAll(func(alias string, r types.StreamRoute) { + rts[alias] = r }) } } - return routes + return rts } func Statistics() map[string]any { diff --git a/internal/docker/idlewatcher/waker.go b/internal/docker/idlewatcher/waker.go index 49b2f070..01e6e60a 100644 --- a/internal/docker/idlewatcher/waker.go +++ b/internal/docker/idlewatcher/waker.go @@ -6,28 +6,31 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/yusing/go-proxy/internal/common" - . "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" + "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/metrics" gphttp "github.com/yusing/go-proxy/internal/net/http" net "github.com/yusing/go-proxy/internal/net/types" - "github.com/yusing/go-proxy/internal/proxy/entry" + route "github.com/yusing/go-proxy/internal/route/types" "github.com/yusing/go-proxy/internal/task" U "github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/watcher/health" "github.com/yusing/go-proxy/internal/watcher/health/monitor" ) -type waker struct { - _ U.NoCopy +type ( + Waker = types.Waker + waker struct { + _ U.NoCopy - rp *gphttp.ReverseProxy - stream net.Stream - hc health.HealthChecker - metric *metrics.Gauge + rp *gphttp.ReverseProxy + stream net.Stream + hc health.HealthChecker + metric *metrics.Gauge - ready atomic.Bool -} + ready atomic.Bool + } +) const ( idleWakerCheckInterval = 100 * time.Millisecond @@ -36,7 +39,7 @@ const ( // TODO: support stream -func newWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReverseProxy, stream net.Stream) (Waker, E.Error) { +func newWaker(providerSubTask task.Task, entry route.Entry, rp *gphttp.ReverseProxy, stream net.Stream) (Waker, E.Error) { hcCfg := entry.HealthCheckConfig() hcCfg.Timeout = idleWakerCheckTimeout @@ -69,11 +72,11 @@ func newWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReversePr } // lifetime should follow route provider. -func NewHTTPWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReverseProxy) (Waker, E.Error) { +func NewHTTPWaker(providerSubTask task.Task, entry route.Entry, rp *gphttp.ReverseProxy) (Waker, E.Error) { return newWaker(providerSubTask, entry, rp, nil) } -func NewStreamWaker(providerSubTask task.Task, entry entry.Entry, stream net.Stream) (Waker, E.Error) { +func NewStreamWaker(providerSubTask task.Task, entry route.Entry, stream net.Stream) (Waker, E.Error) { return newWaker(providerSubTask, entry, nil, stream) } diff --git a/internal/docker/idlewatcher/watcher.go b/internal/docker/idlewatcher/watcher.go index 5426d381..f578029d 100644 --- a/internal/docker/idlewatcher/watcher.go +++ b/internal/docker/idlewatcher/watcher.go @@ -12,7 +12,7 @@ import ( idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/logging" - "github.com/yusing/go-proxy/internal/proxy/entry" + route "github.com/yusing/go-proxy/internal/route/types" "github.com/yusing/go-proxy/internal/task" U "github.com/yusing/go-proxy/internal/utils" F "github.com/yusing/go-proxy/internal/utils/functional" @@ -49,7 +49,7 @@ var ( const dockerReqTimeout = 3 * time.Second -func registerWatcher(providerSubtask task.Task, entry entry.Entry, waker *waker) (*Watcher, error) { +func registerWatcher(providerSubtask task.Task, entry route.Entry, waker *waker) (*Watcher, error) { cfg := entry.IdlewatcherConfig() if cfg.IdleTimeout == 0 { diff --git a/internal/entrypoint/entrypoint.go b/internal/entrypoint/entrypoint.go new file mode 100644 index 00000000..7aa52236 --- /dev/null +++ b/internal/entrypoint/entrypoint.go @@ -0,0 +1,93 @@ +package entrypoint + +import ( + "errors" + "fmt" + "net/http" + "strings" + + "github.com/yusing/go-proxy/internal/net/http/middleware" + "github.com/yusing/go-proxy/internal/net/http/middleware/errorpage" + "github.com/yusing/go-proxy/internal/route/routes" + route "github.com/yusing/go-proxy/internal/route/types" +) + +var findRouteFunc = findRouteAnyDomain + +func SetFindRouteDomains(domains []string) { + if len(domains) == 0 { + findRouteFunc = findRouteAnyDomain + } else { + findRouteFunc = findRouteByDomains(domains) + } +} + +func Handler(w http.ResponseWriter, r *http.Request) { + mux, err := findRouteFunc(r.Host) + if err == nil { + mux.ServeHTTP(w, r) + return + } + // Why use StatusNotFound instead of StatusBadRequest or StatusBadGateway? + // On nginx, when route for domain does not exist, it returns StatusBadGateway. + // Then scraper / scanners will know the subdomain is invalid. + // With StatusNotFound, they won't know whether it's the path, or the subdomain that is invalid. + if !middleware.ServeStaticErrorPageFile(w, r) { + logger.Err(err).Str("method", r.Method).Str("url", r.URL.String()).Msg("request") + errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound) + if ok { + w.WriteHeader(http.StatusNotFound) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if _, err := w.Write(errorPage); err != nil { + logger.Err(err).Msg("failed to write error page") + } + } else { + http.Error(w, err.Error(), http.StatusNotFound) + } + } +} + +func findRouteAnyDomain(host string) (route.HTTPRoute, error) { + hostSplit := strings.Split(host, ".") + n := len(hostSplit) + switch { + case n == 3: + host = hostSplit[0] + case n > 3: + var builder strings.Builder + builder.Grow(2*n - 3) + builder.WriteString(hostSplit[0]) + for _, part := range hostSplit[:n-2] { + builder.WriteRune('.') + builder.WriteString(part) + } + host = builder.String() + default: + return nil, errors.New("missing subdomain in url") + } + if r, ok := routes.GetHTTPRoute(host); ok { + return r, nil + } + return nil, fmt.Errorf("no such route: %s", host) +} + +func findRouteByDomains(domains []string) func(host string) (route.HTTPRoute, error) { + return func(host string) (route.HTTPRoute, error) { + var subdomain string + + for _, domain := range domains { + if strings.HasSuffix(host, domain) { + subdomain = strings.TrimSuffix(host, domain) + break + } + } + + if subdomain != "" { // matched + if r, ok := routes.GetHTTPRoute(subdomain); ok { + return r, nil + } + return nil, fmt.Errorf("no such route: %s", subdomain) + } + return nil, fmt.Errorf("%s does not match any base domain", host) + } +} diff --git a/internal/entrypoint/logger.go b/internal/entrypoint/logger.go new file mode 100644 index 00000000..f6ae78d5 --- /dev/null +++ b/internal/entrypoint/logger.go @@ -0,0 +1,7 @@ +package entrypoint + +import ( + "github.com/yusing/go-proxy/internal/logging" +) + +var logger = logging.With().Str("module", "entrypoint").Logger() diff --git a/internal/net/http/loadbalancer/ip_hash.go b/internal/net/http/loadbalancer/ip_hash.go index f9529327..3df48fd6 100644 --- a/internal/net/http/loadbalancer/ip_hash.go +++ b/internal/net/http/loadbalancer/ip_hash.go @@ -14,7 +14,7 @@ type ipHash struct { *LoadBalancer realIP *middleware.Middleware - pool servers + pool Servers mu sync.Mutex } @@ -26,7 +26,7 @@ func (lb *LoadBalancer) newIPHash() impl { var err E.Error impl.realIP, err = middleware.NewRealIP(lb.Options) if err != nil { - E.LogError("invalid real_ip options, ignoring", err, &impl.Logger) + E.LogError("invalid real_ip options, ignoring", err, &impl.l) } return impl } @@ -60,7 +60,7 @@ func (impl *ipHash) OnRemoveServer(srv *Server) { } } -func (impl *ipHash) ServeHTTP(_ servers, rw http.ResponseWriter, r *http.Request) { +func (impl *ipHash) ServeHTTP(_ Servers, rw http.ResponseWriter, r *http.Request) { if impl.realIP != nil { impl.realIP.ModifyRequest(impl.serveHTTP, rw, r) } else { @@ -72,7 +72,7 @@ func (impl *ipHash) serveHTTP(rw http.ResponseWriter, r *http.Request) { ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { http.Error(rw, "Internal error", http.StatusInternalServerError) - impl.Err(err).Msg("invalid remote address " + r.RemoteAddr) + impl.l.Err(err).Msg("invalid remote address " + r.RemoteAddr) return } idx := hashIP(ip) % uint32(len(impl.pool)) diff --git a/internal/net/http/loadbalancer/least_conn.go b/internal/net/http/loadbalancer/least_conn.go index 8fe88947..3363915f 100644 --- a/internal/net/http/loadbalancer/least_conn.go +++ b/internal/net/http/loadbalancer/least_conn.go @@ -27,18 +27,18 @@ func (impl *leastConn) OnRemoveServer(srv *Server) { impl.nConn.Delete(srv) } -func (impl *leastConn) ServeHTTP(srvs servers, rw http.ResponseWriter, r *http.Request) { +func (impl *leastConn) ServeHTTP(srvs Servers, rw http.ResponseWriter, r *http.Request) { srv := srvs[0] minConn, ok := impl.nConn.Load(srv) if !ok { - impl.Error().Msgf("[BUG] server %s not found", srv.Name) + impl.l.Error().Msgf("[BUG] server %s not found", srv.Name) http.Error(rw, "Internal error", http.StatusInternalServerError) } for i := 1; i < len(srvs); i++ { nConn, ok := impl.nConn.Load(srvs[i]) if !ok { - impl.Error().Msgf("[BUG] server %s not found", srv.Name) + impl.l.Error().Msgf("[BUG] server %s not found", srv.Name) http.Error(rw, "Internal error", http.StatusInternalServerError) } if nConn.Load() < minConn.Load() { diff --git a/internal/net/http/loadbalancer/loadbalancer.go b/internal/net/http/loadbalancer/loadbalancer.go index c7cb8364..46ebae02 100644 --- a/internal/net/http/loadbalancer/loadbalancer.go +++ b/internal/net/http/loadbalancer/loadbalancer.go @@ -7,9 +7,8 @@ import ( "github.com/rs/zerolog" "github.com/yusing/go-proxy/internal/common" - idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" 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/loadbalancer/types" "github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/watcher/health" "github.com/yusing/go-proxy/internal/watcher/health/monitor" @@ -19,19 +18,12 @@ import ( // TODO: support weighted mode. type ( impl interface { - ServeHTTP(srvs servers, rw http.ResponseWriter, r *http.Request) + ServeHTTP(srvs Servers, rw http.ResponseWriter, r *http.Request) OnAddServer(srv *Server) OnRemoveServer(srv *Server) } - Config struct { - Link string `json:"link" yaml:"link"` - Mode Mode `json:"mode" yaml:"mode"` - Weight weightType `json:"weight" yaml:"weight"` - Options middleware.OptionsRaw `json:"options,omitempty" yaml:"options,omitempty"` - } - LoadBalancer struct { - zerolog.Logger + LoadBalancer struct { impl *Config @@ -40,20 +32,20 @@ type ( pool Pool poolMu sync.Mutex - sumWeight weightType + sumWeight Weight startTime time.Time - } - weightType uint16 + l zerolog.Logger + } ) -const maxWeight weightType = 100 +const maxWeight Weight = 100 func New(cfg *Config) *LoadBalancer { lb := &LoadBalancer{ - Logger: logger.With().Str("name", cfg.Link).Logger(), Config: new(Config), - pool: newPool(), + pool: types.NewServerPool(), + l: logger.With().Str("name", cfg.Link).Logger(), } lb.UpdateConfigIfNeeded(cfg) return lb @@ -81,11 +73,11 @@ func (lb *LoadBalancer) Finish(reason any) { func (lb *LoadBalancer) updateImpl() { switch lb.Mode { - case Unset, RoundRobin: + case types.ModeUnset, types.ModeRoundRobin: lb.impl = lb.newRoundRobin() - case LeastConn: + case types.ModeLeastConn: lb.impl = lb.newLeastConn() - case IPHash: + case types.ModeIPHash: lb.impl = lb.newIPHash() default: // should happen in test only lb.impl = lb.newRoundRobin() @@ -102,10 +94,10 @@ func (lb *LoadBalancer) UpdateConfigIfNeeded(cfg *Config) { lb.Link = cfg.Link - if lb.Mode == Unset && cfg.Mode != Unset { + if lb.Mode == types.ModeUnset && cfg.Mode != types.ModeUnset { lb.Mode = cfg.Mode if !lb.Mode.ValidateUpdate() { - lb.Error().Msgf("invalid mode %q, fallback to %q", cfg.Mode, lb.Mode) + lb.l.Error().Msgf("invalid mode %q, fallback to %q", cfg.Mode, lb.Mode) } lb.updateImpl() } @@ -135,7 +127,7 @@ func (lb *LoadBalancer) AddServer(srv *Server) { lb.rebalance() lb.impl.OnAddServer(srv) - lb.Debug(). + lb.l.Debug(). Str("action", "add"). Str("server", srv.Name). Msgf("%d servers available", lb.pool.Size()) @@ -155,7 +147,7 @@ func (lb *LoadBalancer) RemoveServer(srv *Server) { lb.rebalance() lb.impl.OnRemoveServer(srv) - lb.Debug(). + lb.l.Debug(). Str("action", "remove"). Str("server", srv.Name). Msgf("%d servers left", lb.pool.Size()) @@ -174,8 +166,8 @@ func (lb *LoadBalancer) rebalance() { return } if lb.sumWeight == 0 { // distribute evenly - weightEach := maxWeight / weightType(lb.pool.Size()) - remainder := maxWeight % weightType(lb.pool.Size()) + weightEach := maxWeight / Weight(lb.pool.Size()) + remainder := maxWeight % Weight(lb.pool.Size()) lb.pool.RangeAll(func(_ string, s *Server) { s.Weight = weightEach lb.sumWeight += weightEach @@ -192,7 +184,7 @@ func (lb *LoadBalancer) rebalance() { lb.sumWeight = 0 lb.pool.RangeAll(func(_ string, s *Server) { - s.Weight = weightType(float64(s.Weight) * scaleFactor) + s.Weight = Weight(float64(s.Weight) * scaleFactor) lb.sumWeight += s.Weight }) @@ -226,13 +218,7 @@ func (lb *LoadBalancer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { if r.Header.Get(common.HeaderCheckRedirect) != "" { // wake all servers for _, srv := range srvs { - // wake only if server implements Waker - waker, ok := srv.handler.(idlewatcher.Waker) - if ok { - if err := waker.Wake(); err != nil { - lb.Err(err).Msgf("failed to wake server %s", srv.Name) - } - } + srv.TryWake() } } lb.impl.ServeHTTP(srvs, rw, r) @@ -246,7 +232,7 @@ func (lb *LoadBalancer) Uptime() time.Duration { func (lb *LoadBalancer) MarshalJSON() ([]byte, error) { extra := make(map[string]any) lb.pool.RangeAll(func(k string, v *Server) { - extra[v.Name] = v.healthMon + extra[v.Name] = v.HealthMonitor() }) return (&monitor.JSONRepresentation{ diff --git a/internal/net/http/loadbalancer/loadbalancer_test.go b/internal/net/http/loadbalancer/loadbalancer_test.go index b180c6d7..234130c8 100644 --- a/internal/net/http/loadbalancer/loadbalancer_test.go +++ b/internal/net/http/loadbalancer/loadbalancer_test.go @@ -3,13 +3,14 @@ package loadbalancer import ( "testing" + loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types" . "github.com/yusing/go-proxy/internal/utils/testing" ) func TestRebalance(t *testing.T) { t.Parallel() t.Run("zero", func(t *testing.T) { - lb := New(new(Config)) + lb := New(new(loadbalance.Config)) for range 10 { lb.AddServer(&Server{}) } @@ -17,25 +18,25 @@ func TestRebalance(t *testing.T) { ExpectEqual(t, lb.sumWeight, maxWeight) }) t.Run("less", func(t *testing.T) { - lb := New(new(Config)) - lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .1)}) - lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .2)}) - lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .3)}) - lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .2)}) - lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .1)}) + lb := New(new(loadbalance.Config)) + lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .1)}) + lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .2)}) + lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .3)}) + lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .2)}) + lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .1)}) lb.rebalance() // t.Logf("%s", U.Must(json.MarshalIndent(lb.pool, "", " "))) ExpectEqual(t, lb.sumWeight, maxWeight) }) t.Run("more", func(t *testing.T) { - lb := New(new(Config)) - lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .1)}) - lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .2)}) - lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .3)}) - lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .4)}) - lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .3)}) - lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .2)}) - lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .1)}) + lb := New(new(loadbalance.Config)) + lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .1)}) + lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .2)}) + lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .3)}) + lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .4)}) + lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .3)}) + lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .2)}) + lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .1)}) lb.rebalance() // t.Logf("%s", U.Must(json.MarshalIndent(lb.pool, "", " "))) ExpectEqual(t, lb.sumWeight, maxWeight) diff --git a/internal/net/http/loadbalancer/mode.go b/internal/net/http/loadbalancer/mode.go deleted file mode 100644 index cea093d7..00000000 --- a/internal/net/http/loadbalancer/mode.go +++ /dev/null @@ -1,32 +0,0 @@ -package loadbalancer - -import ( - "github.com/yusing/go-proxy/internal/utils/strutils" -) - -type Mode string - -const ( - Unset Mode = "" - RoundRobin Mode = "roundrobin" - LeastConn Mode = "leastconn" - IPHash Mode = "iphash" -) - -func (mode *Mode) ValidateUpdate() bool { - switch strutils.ToLowerNoSnake(string(*mode)) { - case "": - return true - case string(RoundRobin): - *mode = RoundRobin - return true - case string(LeastConn): - *mode = LeastConn - return true - case string(IPHash): - *mode = IPHash - return true - } - *mode = RoundRobin - return false -} diff --git a/internal/net/http/loadbalancer/round_robin.go b/internal/net/http/loadbalancer/round_robin.go index 41e70c8a..494c21e0 100644 --- a/internal/net/http/loadbalancer/round_robin.go +++ b/internal/net/http/loadbalancer/round_robin.go @@ -13,7 +13,7 @@ func (*LoadBalancer) newRoundRobin() impl { return &roundRobin{} } func (lb *roundRobin) OnAddServer(srv *Server) {} func (lb *roundRobin) OnRemoveServer(srv *Server) {} -func (lb *roundRobin) ServeHTTP(srvs servers, rw http.ResponseWriter, r *http.Request) { +func (lb *roundRobin) ServeHTTP(srvs Servers, rw http.ResponseWriter, r *http.Request) { index := lb.index.Add(1) % uint32(len(srvs)) srvs[index].ServeHTTP(rw, r) if lb.index.Load() >= 2*uint32(len(srvs)) { diff --git a/internal/net/http/loadbalancer/types.go b/internal/net/http/loadbalancer/types.go new file mode 100644 index 00000000..aa603697 --- /dev/null +++ b/internal/net/http/loadbalancer/types.go @@ -0,0 +1,14 @@ +package loadbalancer + +import ( + "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types" +) + +type ( + Server = types.Server + Servers = types.Servers + Pool = types.Pool + Weight = types.Weight + Config = types.Config + Mode = types.Mode +) diff --git a/internal/net/http/loadbalancer/types/config.go b/internal/net/http/loadbalancer/types/config.go new file mode 100644 index 00000000..4939188c --- /dev/null +++ b/internal/net/http/loadbalancer/types/config.go @@ -0,0 +1,8 @@ +package types + +type Config struct { + Link string `json:"link" yaml:"link"` + Mode Mode `json:"mode" yaml:"mode"` + Weight Weight `json:"weight" yaml:"weight"` + Options map[string]any `json:"options,omitempty" yaml:"options,omitempty"` +} diff --git a/internal/net/http/loadbalancer/types/mode.go b/internal/net/http/loadbalancer/types/mode.go new file mode 100644 index 00000000..210275a5 --- /dev/null +++ b/internal/net/http/loadbalancer/types/mode.go @@ -0,0 +1,32 @@ +package types + +import ( + "github.com/yusing/go-proxy/internal/utils/strutils" +) + +type Mode string + +const ( + ModeUnset Mode = "" + ModeRoundRobin Mode = "roundrobin" + ModeLeastConn Mode = "leastconn" + ModeIPHash Mode = "iphash" +) + +func (mode *Mode) ValidateUpdate() bool { + switch strutils.ToLowerNoSnake(string(*mode)) { + case "": + return true + case string(ModeRoundRobin): + *mode = ModeRoundRobin + return true + case string(ModeLeastConn): + *mode = ModeLeastConn + return true + case string(ModeIPHash): + *mode = ModeIPHash + return true + } + *mode = ModeRoundRobin + return false +} diff --git a/internal/net/http/loadbalancer/server.go b/internal/net/http/loadbalancer/types/server.go similarity index 61% rename from internal/net/http/loadbalancer/server.go rename to internal/net/http/loadbalancer/types/server.go index f8a96233..c6b95377 100644 --- a/internal/net/http/loadbalancer/server.go +++ b/internal/net/http/loadbalancer/types/server.go @@ -1,9 +1,10 @@ -package loadbalancer +package types import ( "net/http" "time" + idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" "github.com/yusing/go-proxy/internal/net/types" U "github.com/yusing/go-proxy/internal/utils" F "github.com/yusing/go-proxy/internal/utils/functional" @@ -16,18 +17,18 @@ type ( Name string URL types.URL - Weight weightType + Weight Weight handler http.Handler healthMon health.HealthMonitor } - servers = []*Server + Servers = []*Server Pool = F.Map[string, *Server] ) -var newPool = F.NewMap[Pool] +var NewServerPool = F.NewMap[Pool] -func NewServer(name string, url types.URL, weight weightType, handler http.Handler, healthMon health.HealthMonitor) *Server { +func NewServer(name string, url types.URL, weight Weight, handler http.Handler, healthMon health.HealthMonitor) *Server { srv := &Server{ Name: name, URL: url, @@ -53,3 +54,17 @@ func (srv *Server) Status() health.Status { func (srv *Server) Uptime() time.Duration { return srv.healthMon.Uptime() } + +func (srv *Server) TryWake() error { + waker, ok := srv.handler.(idlewatcher.Waker) + if ok { + if err := waker.Wake(); err != nil { + return err + } + } + return nil +} + +func (srv *Server) HealthMonitor() health.HealthMonitor { + return srv.healthMon +} diff --git a/internal/net/http/loadbalancer/types/weight.go b/internal/net/http/loadbalancer/types/weight.go new file mode 100644 index 00000000..2bf7d848 --- /dev/null +++ b/internal/net/http/loadbalancer/types/weight.go @@ -0,0 +1,3 @@ +package types + +type Weight uint16 diff --git a/internal/server/server.go b/internal/net/http/server/server.go similarity index 100% rename from internal/server/server.go rename to internal/net/http/server/server.go diff --git a/internal/proxy/entry/entry.go b/internal/route/entry/entry.go similarity index 66% rename from internal/proxy/entry/entry.go rename to internal/route/entry/entry.go index 66a237e2..27c0c70f 100644 --- a/internal/proxy/entry/entry.go +++ b/internal/route/entry/entry.go @@ -1,25 +1,14 @@ package entry import ( - idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" E "github.com/yusing/go-proxy/internal/error" - "github.com/yusing/go-proxy/internal/net/http/loadbalancer" - net "github.com/yusing/go-proxy/internal/net/types" - T "github.com/yusing/go-proxy/internal/proxy/fields" - "github.com/yusing/go-proxy/internal/watcher/health" + route "github.com/yusing/go-proxy/internal/route/types" ) -type Entry interface { - TargetName() string - TargetURL() net.URL - RawEntry() *RawEntry - LoadBalanceConfig() *loadbalancer.Config - HealthCheckConfig() *health.HealthCheckConfig - IdlewatcherConfig() *idlewatcher.Config -} +type Entry = route.Entry -func ValidateEntry(m *RawEntry) (Entry, E.Error) { - scheme, err := T.NewScheme(m.Scheme) +func ValidateEntry(m *route.RawEntry) (Entry, E.Error) { + scheme, err := route.NewScheme(m.Scheme) if err != nil { return nil, E.From(err) } diff --git a/internal/proxy/entry/reverse_proxy.go b/internal/route/entry/reverse_proxy.go similarity index 67% rename from internal/proxy/entry/reverse_proxy.go rename to internal/route/entry/reverse_proxy.go index e16278ea..4f8a8a8a 100644 --- a/internal/proxy/entry/reverse_proxy.go +++ b/internal/route/entry/reverse_proxy.go @@ -7,22 +7,22 @@ import ( "github.com/yusing/go-proxy/internal/docker" idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" E "github.com/yusing/go-proxy/internal/error" - "github.com/yusing/go-proxy/internal/net/http/loadbalancer" + loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types" net "github.com/yusing/go-proxy/internal/net/types" - "github.com/yusing/go-proxy/internal/proxy/fields" + route "github.com/yusing/go-proxy/internal/route/types" "github.com/yusing/go-proxy/internal/watcher/health" ) type ReverseProxyEntry struct { // real model after validation - Raw *RawEntry `json:"raw"` + Raw *route.RawEntry `json:"raw"` - Alias fields.Alias `json:"alias"` - Scheme fields.Scheme `json:"scheme"` + Alias route.Alias `json:"alias"` + Scheme route.Scheme `json:"scheme"` URL net.URL `json:"url"` NoTLSVerify bool `json:"no_tls_verify,omitempty"` - PathPatterns fields.PathPatterns `json:"path_patterns,omitempty"` + PathPatterns route.PathPatterns `json:"path_patterns,omitempty"` HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"` - LoadBalance *loadbalancer.Config `json:"load_balance,omitempty"` + LoadBalance *loadbalance.Config `json:"load_balance,omitempty"` Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty"` /* Docker only */ @@ -37,11 +37,11 @@ func (rp *ReverseProxyEntry) TargetURL() net.URL { return rp.URL } -func (rp *ReverseProxyEntry) RawEntry() *RawEntry { +func (rp *ReverseProxyEntry) RawEntry() *route.RawEntry { return rp.Raw } -func (rp *ReverseProxyEntry) LoadBalanceConfig() *loadbalancer.Config { +func (rp *ReverseProxyEntry) LoadBalanceConfig() *loadbalance.Config { return rp.LoadBalance } @@ -53,7 +53,7 @@ func (rp *ReverseProxyEntry) IdlewatcherConfig() *idlewatcher.Config { return rp.Idlewatcher } -func validateRPEntry(m *RawEntry, s fields.Scheme, errs *E.Builder) *ReverseProxyEntry { +func validateRPEntry(m *route.RawEntry, s route.Scheme, errs *E.Builder) *ReverseProxyEntry { cont := m.Container if cont == nil { cont = docker.DummyContainer @@ -64,9 +64,9 @@ func validateRPEntry(m *RawEntry, s fields.Scheme, errs *E.Builder) *ReverseProx lb = nil } - host := E.Collect(errs, fields.ValidateHost, m.Host) - port := E.Collect(errs, fields.ValidatePort, m.Port) - pathPats := E.Collect(errs, fields.ValidatePathPatterns, m.PathPatterns) + host := E.Collect(errs, route.ValidateHost, m.Host) + port := E.Collect(errs, route.ValidatePort, m.Port) + pathPats := E.Collect(errs, route.ValidatePathPatterns, m.PathPatterns) url := E.Collect(errs, url.Parse, fmt.Sprintf("%s://%s:%d", s, host, port)) iwCfg := E.Collect(errs, idlewatcher.ValidateConfig, cont) @@ -76,7 +76,7 @@ func validateRPEntry(m *RawEntry, s fields.Scheme, errs *E.Builder) *ReverseProx return &ReverseProxyEntry{ Raw: m, - Alias: fields.Alias(m.Alias), + Alias: route.Alias(m.Alias), Scheme: s, URL: net.NewURL(url), NoTLSVerify: m.NoTLSVerify, diff --git a/internal/proxy/entry/stream.go b/internal/route/entry/stream.go similarity index 63% rename from internal/proxy/entry/stream.go rename to internal/route/entry/stream.go index 62483401..936dd428 100644 --- a/internal/proxy/entry/stream.go +++ b/internal/route/entry/stream.go @@ -6,20 +6,20 @@ import ( "github.com/yusing/go-proxy/internal/docker" idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" E "github.com/yusing/go-proxy/internal/error" - "github.com/yusing/go-proxy/internal/net/http/loadbalancer" + loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types" net "github.com/yusing/go-proxy/internal/net/types" - "github.com/yusing/go-proxy/internal/proxy/fields" + route "github.com/yusing/go-proxy/internal/route/types" "github.com/yusing/go-proxy/internal/watcher/health" ) type StreamEntry struct { - Raw *RawEntry `json:"raw"` + Raw *route.RawEntry `json:"raw"` - Alias fields.Alias `json:"alias"` - Scheme fields.StreamScheme `json:"scheme"` + Alias route.Alias `json:"alias"` + Scheme route.StreamScheme `json:"scheme"` URL net.URL `json:"url"` - Host fields.Host `json:"host,omitempty"` - Port fields.StreamPort `json:"port,omitempty"` + Host route.Host `json:"host,omitempty"` + Port route.StreamPort `json:"port,omitempty"` HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"` /* Docker only */ @@ -34,11 +34,11 @@ func (s *StreamEntry) TargetURL() net.URL { return s.URL } -func (s *StreamEntry) RawEntry() *RawEntry { +func (s *StreamEntry) RawEntry() *route.RawEntry { return s.Raw } -func (s *StreamEntry) LoadBalanceConfig() *loadbalancer.Config { +func (s *StreamEntry) LoadBalanceConfig() *loadbalance.Config { // TODO: support stream load balance return nil } @@ -51,15 +51,15 @@ func (s *StreamEntry) IdlewatcherConfig() *idlewatcher.Config { return s.Idlewatcher } -func validateStreamEntry(m *RawEntry, errs *E.Builder) *StreamEntry { +func validateStreamEntry(m *route.RawEntry, errs *E.Builder) *StreamEntry { cont := m.Container if cont == nil { cont = docker.DummyContainer } - host := E.Collect(errs, fields.ValidateHost, m.Host) - port := E.Collect(errs, fields.ValidateStreamPort, m.Port) - scheme := E.Collect(errs, fields.ValidateStreamScheme, m.Scheme) + host := E.Collect(errs, route.ValidateHost, m.Host) + port := E.Collect(errs, route.ValidateStreamPort, m.Port) + scheme := E.Collect(errs, route.ValidateStreamScheme, m.Scheme) url := E.Collect(errs, net.ParseURL, fmt.Sprintf("%s://%s:%d", scheme.ListeningScheme, host, port.ProxyPort)) idleWatcherCfg := E.Collect(errs, idlewatcher.ValidateConfig, cont) @@ -69,7 +69,7 @@ func validateStreamEntry(m *RawEntry, errs *E.Builder) *StreamEntry { return &StreamEntry{ Raw: m, - Alias: fields.Alias(m.Alias), + Alias: route.Alias(m.Alias), Scheme: *scheme, URL: url, Host: host, diff --git a/internal/route/http.go b/internal/route/http.go index d9ce3054..614a18fe 100755 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -1,10 +1,7 @@ package route import ( - "errors" - "fmt" "net/http" - "strings" "github.com/rs/zerolog" "github.com/yusing/go-proxy/internal/common" @@ -12,12 +9,12 @@ import ( E "github.com/yusing/go-proxy/internal/error" gphttp "github.com/yusing/go-proxy/internal/net/http" "github.com/yusing/go-proxy/internal/net/http/loadbalancer" + loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types" "github.com/yusing/go-proxy/internal/net/http/middleware" - "github.com/yusing/go-proxy/internal/net/http/middleware/errorpage" - "github.com/yusing/go-proxy/internal/proxy/entry" - PT "github.com/yusing/go-proxy/internal/proxy/fields" + "github.com/yusing/go-proxy/internal/route/entry" + "github.com/yusing/go-proxy/internal/route/routes" + route "github.com/yusing/go-proxy/internal/route/types" "github.com/yusing/go-proxy/internal/task" - F "github.com/yusing/go-proxy/internal/utils/functional" "github.com/yusing/go-proxy/internal/watcher/health" "github.com/yusing/go-proxy/internal/watcher/health/monitor" ) @@ -38,27 +35,10 @@ type ( l zerolog.Logger } - SubdomainKey = PT.Alias + SubdomainKey = route.Alias ) -var ( - findMuxFunc = findMuxAnyDomain - - httpRoutes = F.NewMapOf[string, *HTTPRoute]() - // globalMux = http.NewServeMux() // TODO: support regex subdomain matching. -) - -func GetReverseProxies() F.Map[string, *HTTPRoute] { - return httpRoutes -} - -func SetFindMuxDomains(domains []string) { - if len(domains) == 0 { - findMuxFunc = findMuxAnyDomain - } else { - findMuxFunc = findMuxByDomains(domains) - } -} +// var globalMux = http.NewServeMux() // TODO: support regex subdomain matching. func NewHTTPRoute(entry *entry.ReverseProxyEntry) (impl, E.Error) { var trans *http.Transport @@ -141,9 +121,9 @@ func (r *HTTPRoute) Start(providerSubtask task.Task) E.Error { if entry.UseLoadBalance(r) { r.addToLoadBalancer() } else { - httpRoutes.Store(string(r.Alias), r) + routes.SetHTTPRoute(string(r.Alias), r) r.task.OnFinished("remove from route table", func() { - httpRoutes.Delete(string(r.Alias)) + routes.DeleteHTTPRoute(string(r.Alias)) }) } @@ -164,7 +144,8 @@ func (r *HTTPRoute) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (r *HTTPRoute) addToLoadBalancer() { var lb *loadbalancer.LoadBalancer - linked, ok := httpRoutes.Load(r.LoadBalance.Link) + l, ok := routes.GetHTTPRoute(r.LoadBalance.Link) + linked := l.(*HTTPRoute) if ok { lb = linked.loadBalancer lb.UpdateConfigIfNeeded(r.LoadBalance) @@ -175,96 +156,26 @@ func (r *HTTPRoute) addToLoadBalancer() { lb = loadbalancer.New(r.LoadBalance) lbTask := r.task.Parent().Subtask("loadbalancer " + r.LoadBalance.Link) lbTask.OnCancel("remove lb from routes", func() { - httpRoutes.Delete(r.LoadBalance.Link) + routes.DeleteHTTPRoute(r.LoadBalance.Link) }) lb.Start(lbTask) linked = &HTTPRoute{ ReverseProxyEntry: &entry.ReverseProxyEntry{ - Raw: &entry.RawEntry{ + Raw: &route.RawEntry{ Homepage: r.Raw.Homepage, }, - Alias: PT.Alias(lb.Link), + Alias: route.Alias(lb.Link), }, HealthMon: lb, loadBalancer: lb, handler: lb, } - httpRoutes.Store(r.LoadBalance.Link, linked) + routes.SetHTTPRoute(r.LoadBalance.Link, linked) } r.loadBalancer = lb - r.server = loadbalancer.NewServer(r.task.String(), r.rp.TargetURL, r.LoadBalance.Weight, r.handler, r.HealthMon) + r.server = loadbalance.NewServer(r.task.String(), r.rp.TargetURL, r.LoadBalance.Weight, r.handler, r.HealthMon) lb.AddServer(r.server) r.task.OnCancel("remove server from lb", func() { lb.RemoveServer(r.server) }) } - -func ProxyHandler(w http.ResponseWriter, r *http.Request) { - mux, err := findMuxFunc(r.Host) - if err == nil { - mux.ServeHTTP(w, r) - return - } - // Why use StatusNotFound instead of StatusBadRequest or StatusBadGateway? - // On nginx, when route for domain does not exist, it returns StatusBadGateway. - // Then scraper / scanners will know the subdomain is invalid. - // With StatusNotFound, they won't know whether it's the path, or the subdomain that is invalid. - if !middleware.ServeStaticErrorPageFile(w, r) { - logger.Err(err).Str("method", r.Method).Str("url", r.URL.String()).Msg("request") - errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound) - if ok { - w.WriteHeader(http.StatusNotFound) - w.Header().Set("Content-Type", "text/html; charset=utf-8") - if _, err := w.Write(errorPage); err != nil { - logger.Err(err).Msg("failed to write error page") - } - } else { - http.Error(w, err.Error(), http.StatusNotFound) - } - } -} - -func findMuxAnyDomain(host string) (http.Handler, error) { - hostSplit := strings.Split(host, ".") - n := len(hostSplit) - switch { - case n == 3: - host = hostSplit[0] - case n > 3: - var builder strings.Builder - builder.Grow(2*n - 3) - builder.WriteString(hostSplit[0]) - for _, part := range hostSplit[:n-2] { - builder.WriteRune('.') - builder.WriteString(part) - } - host = builder.String() - default: - return nil, errors.New("missing subdomain in url") - } - if r, ok := httpRoutes.Load(host); ok { - return r.handler, nil - } - return nil, fmt.Errorf("no such route: %s", host) -} - -func findMuxByDomains(domains []string) func(host string) (http.Handler, error) { - return func(host string) (http.Handler, error) { - var subdomain string - - for _, domain := range domains { - if strings.HasSuffix(host, domain) { - subdomain = strings.TrimSuffix(host, domain) - break - } - } - - if subdomain != "" { // matched - if r, ok := httpRoutes.Load(subdomain); ok { - return r.handler, nil - } - return nil, fmt.Errorf("no such route: %s", subdomain) - } - return nil, fmt.Errorf("%s does not match any base domain", host) - } -} diff --git a/internal/route/provider/docker.go b/internal/route/provider/docker.go index 94e04262..bbdf3da1 100755 --- a/internal/route/provider/docker.go +++ b/internal/route/provider/docker.go @@ -9,7 +9,6 @@ import ( "github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/docker" E "github.com/yusing/go-proxy/internal/error" - "github.com/yusing/go-proxy/internal/proxy/entry" "github.com/yusing/go-proxy/internal/route" U "github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils/strutils" @@ -55,7 +54,7 @@ func (p *DockerProvider) NewWatcher() watcher.Watcher { func (p *DockerProvider) loadRoutesImpl() (route.Routes, E.Error) { routes := route.NewRoutes() - entries := entry.NewProxyEntries() + entries := route.NewProxyEntries() containers, err := docker.ListContainers(p.dockerHost) if err != nil { @@ -78,7 +77,7 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, E.Error) { // there may be some valid entries in `en` dups := entries.MergeFrom(newEntries) // add the duplicate proxy entries to the error - dups.RangeAll(func(k string, v *entry.RawEntry) { + dups.RangeAll(func(k string, v *route.RawEntry) { errs.Addf("duplicated alias %s", k) }) } @@ -98,8 +97,8 @@ func (p *DockerProvider) shouldIgnore(container *docker.Container) bool { // Returns a list of proxy entries for a container. // Always non-nil. -func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container) (entries entry.RawEntries, _ E.Error) { - entries = entry.NewProxyEntries() +func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container) (entries route.RawEntries, _ E.Error) { + entries = route.NewProxyEntries() if p.shouldIgnore(container) { return @@ -107,7 +106,7 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container) // init entries map for all aliases for _, a := range container.Aliases { - entries.Store(a, &entry.RawEntry{ + entries.Store(a, &route.RawEntry{ Alias: a, Container: container, }) @@ -154,9 +153,9 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container) } // init entry if not exist - var en *entry.RawEntry + var en *route.RawEntry if en, ok = entries.Load(alias); !ok { - en = &entry.RawEntry{ + en = &route.RawEntry{ Alias: alias, Container: container, } @@ -172,7 +171,7 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container) } } if wildcardProps != nil { - entries.RangeAll(func(alias string, re *entry.RawEntry) { + entries.RangeAll(func(alias string, re *route.RawEntry) { if err := U.Deserialize(wildcardProps, re); err != nil { errs.Add(err.Subject(alias)) } diff --git a/internal/route/provider/docker_test.go b/internal/route/provider/docker_test.go index a5147c8d..0f6f4966 100644 --- a/internal/route/provider/docker_test.go +++ b/internal/route/provider/docker_test.go @@ -10,8 +10,8 @@ import ( "github.com/yusing/go-proxy/internal/common" D "github.com/yusing/go-proxy/internal/docker" E "github.com/yusing/go-proxy/internal/error" - "github.com/yusing/go-proxy/internal/proxy/entry" - T "github.com/yusing/go-proxy/internal/proxy/fields" + "github.com/yusing/go-proxy/internal/route/entry" + T "github.com/yusing/go-proxy/internal/route/types" . "github.com/yusing/go-proxy/internal/utils/testing" ) diff --git a/internal/route/provider/event_handler.go b/internal/route/provider/event_handler.go index 9a673737..8af8b61e 100644 --- a/internal/route/provider/event_handler.go +++ b/internal/route/provider/event_handler.go @@ -3,8 +3,8 @@ package provider import ( "github.com/yusing/go-proxy/internal/common" E "github.com/yusing/go-proxy/internal/error" - "github.com/yusing/go-proxy/internal/proxy/entry" "github.com/yusing/go-proxy/internal/route" + "github.com/yusing/go-proxy/internal/route/entry" "github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/watcher" ) diff --git a/internal/route/provider/file.go b/internal/route/provider/file.go index cca3372f..ffa17886 100644 --- a/internal/route/provider/file.go +++ b/internal/route/provider/file.go @@ -7,8 +7,7 @@ import ( "github.com/rs/zerolog" "github.com/yusing/go-proxy/internal/common" E "github.com/yusing/go-proxy/internal/error" - "github.com/yusing/go-proxy/internal/proxy/entry" - R "github.com/yusing/go-proxy/internal/route" + "github.com/yusing/go-proxy/internal/route" U "github.com/yusing/go-proxy/internal/utils" W "github.com/yusing/go-proxy/internal/watcher" ) @@ -44,9 +43,9 @@ func (p *FileProvider) Logger() *zerolog.Logger { return &p.l } -func (p *FileProvider) loadRoutesImpl() (R.Routes, E.Error) { - routes := R.NewRoutes() - entries := entry.NewProxyEntries() +func (p *FileProvider) loadRoutesImpl() (route.Routes, E.Error) { + routes := route.NewRoutes() + entries := route.NewProxyEntries() data, err := os.ReadFile(p.path) if err != nil { @@ -61,7 +60,7 @@ func (p *FileProvider) loadRoutesImpl() (R.Routes, E.Error) { E.LogWarn("validation failure", err.Subject(p.fileName)) } - return R.FromEntries(entries) + return route.FromEntries(entries) } func (p *FileProvider) NewWatcher() W.Watcher { diff --git a/internal/route/route.go b/internal/route/route.go index d71962ff..f9c3dd80 100755 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -4,7 +4,8 @@ import ( "github.com/yusing/go-proxy/internal/docker" E "github.com/yusing/go-proxy/internal/error" url "github.com/yusing/go-proxy/internal/net/types" - "github.com/yusing/go-proxy/internal/proxy/entry" + "github.com/yusing/go-proxy/internal/route/entry" + "github.com/yusing/go-proxy/internal/route/types" "github.com/yusing/go-proxy/internal/task" U "github.com/yusing/go-proxy/internal/utils" F "github.com/yusing/go-proxy/internal/utils/functional" @@ -16,7 +17,7 @@ type ( _ U.NoCopy impl Type RouteType - Entry *entry.RawEntry + Entry *RawEntry } Routes = F.Map[string, *Route] @@ -27,6 +28,8 @@ type ( String() string TargetURL() url.URL } + RawEntry = types.RawEntry + RawEntries = types.RawEntries ) const ( @@ -36,6 +39,7 @@ const ( // function alias. var NewRoutes = F.NewMap[Routes] +var NewProxyEntries = types.NewProxyEntries func (rt *Route) Container() *docker.Container { if rt.Entry.Container == nil { @@ -44,7 +48,7 @@ func (rt *Route) Container() *docker.Container { return rt.Entry.Container } -func NewRoute(raw *entry.RawEntry) (*Route, E.Error) { +func NewRoute(raw *RawEntry) (*Route, E.Error) { raw.Finalize() en, err := entry.ValidateEntry(raw) if err != nil { @@ -74,11 +78,11 @@ func NewRoute(raw *entry.RawEntry) (*Route, E.Error) { }, nil } -func FromEntries(entries entry.RawEntries) (Routes, E.Error) { +func FromEntries(entries RawEntries) (Routes, E.Error) { b := E.NewBuilder("errors in routes") routes := NewRoutes() - entries.RangeAllParallel(func(alias string, en *entry.RawEntry) { + entries.RangeAllParallel(func(alias string, en *RawEntry) { en.Alias = alias r, err := NewRoute(en) switch { diff --git a/internal/route/metrics.go b/internal/route/routes/metrics.go similarity index 89% rename from internal/route/metrics.go rename to internal/route/routes/metrics.go index 93ac6309..755d8b3b 100644 --- a/internal/route/metrics.go +++ b/internal/route/routes/metrics.go @@ -1,4 +1,4 @@ -package route +package routes import "github.com/yusing/go-proxy/internal/metrics" diff --git a/internal/route/routes/routes.go b/internal/route/routes/routes.go new file mode 100644 index 00000000..4d0165f0 --- /dev/null +++ b/internal/route/routes/routes.go @@ -0,0 +1,43 @@ +package routes + +import ( + "github.com/yusing/go-proxy/internal/route/types" + F "github.com/yusing/go-proxy/internal/utils/functional" +) + +var ( + httpRoutes = F.NewMapOf[string, types.HTTPRoute]() + streamRoutes = F.NewMapOf[string, types.StreamRoute]() +) + +func GetHTTPRoutes() F.Map[string, types.HTTPRoute] { + return httpRoutes +} + +func GetStreamRoutes() F.Map[string, types.StreamRoute] { + return streamRoutes +} + +func GetHTTPRoute(alias string) (types.HTTPRoute, bool) { + return httpRoutes.Load(alias) +} + +func GetStreamRoute(alias string) (types.StreamRoute, bool) { + return streamRoutes.Load(alias) +} + +func SetHTTPRoute(alias string, r types.HTTPRoute) { + httpRoutes.Store(alias, r) +} + +func SetStreamRoute(alias string, r types.StreamRoute) { + streamRoutes.Store(alias, r) +} + +func DeleteHTTPRoute(alias string) { + httpRoutes.Delete(alias) +} + +func DeleteStreamRoute(alias string) { + streamRoutes.Delete(alias) +} diff --git a/internal/route/stream.go b/internal/route/stream.go index 4884ae76..da87b6b7 100755 --- a/internal/route/stream.go +++ b/internal/route/stream.go @@ -9,9 +9,9 @@ import ( "github.com/yusing/go-proxy/internal/docker/idlewatcher" E "github.com/yusing/go-proxy/internal/error" net "github.com/yusing/go-proxy/internal/net/types" - "github.com/yusing/go-proxy/internal/proxy/entry" + "github.com/yusing/go-proxy/internal/route/entry" + "github.com/yusing/go-proxy/internal/route/routes" "github.com/yusing/go-proxy/internal/task" - F "github.com/yusing/go-proxy/internal/utils/functional" "github.com/yusing/go-proxy/internal/watcher/health" "github.com/yusing/go-proxy/internal/watcher/health/monitor" ) @@ -19,7 +19,7 @@ import ( type StreamRoute struct { *entry.StreamEntry - stream net.Stream + net.Stream HealthMon health.HealthMonitor `json:"health"` @@ -28,12 +28,6 @@ type StreamRoute struct { l zerolog.Logger } -var streamRoutes = F.NewMapOf[string, *StreamRoute]() - -func GetStreamProxies() F.Map[string, *StreamRoute] { - return streamRoutes -} - func NewStreamRoute(entry *entry.StreamEntry) (impl, E.Error) { // TODO: support non-coherent scheme if !entry.Scheme.IsCoherent() { @@ -60,29 +54,29 @@ func (r *StreamRoute) Start(providerSubtask task.Task) E.Error { } r.task = providerSubtask - r.stream = NewStream(r) + r.Stream = NewStream(r) switch { case entry.UseIdleWatcher(r): wakerTask := providerSubtask.Parent().Subtask("waker for " + string(r.Alias)) - waker, err := idlewatcher.NewStreamWaker(wakerTask, r.StreamEntry, r.stream) + waker, err := idlewatcher.NewStreamWaker(wakerTask, r.StreamEntry, r.Stream) if err != nil { r.task.Finish(err) return err } - r.stream = waker + r.Stream = waker r.HealthMon = waker case entry.UseHealthCheck(r): r.HealthMon = monitor.NewRawHealthMonitor(r.TargetURL(), r.HealthCheck) } - if err := r.stream.Setup(); err != nil { + if err := r.Stream.Setup(); err != nil { r.task.Finish(err) return E.From(err) } r.task.OnFinished("close stream", func() { - if err := r.stream.Close(); err != nil { + if err := r.Stream.Close(); err != nil { E.LogError("close stream failed", err, &r.l) } }) @@ -101,9 +95,9 @@ func (r *StreamRoute) Start(providerSubtask task.Task) E.Error { go r.acceptConnections() - streamRoutes.Store(string(r.Alias), r) + routes.SetStreamRoute(string(r.Alias), r) r.task.OnFinished("remove from route table", func() { - streamRoutes.Delete(string(r.Alias)) + routes.DeleteStreamRoute(string(r.Alias)) }) return nil } @@ -120,7 +114,7 @@ func (r *StreamRoute) acceptConnections() { case <-r.task.Context().Done(): return default: - conn, err := r.stream.Accept() + conn, err := r.Stream.Accept() if err != nil { select { case <-r.task.Context().Done(): @@ -135,7 +129,7 @@ func (r *StreamRoute) acceptConnections() { } connTask := r.task.Subtask("connection") go func() { - err := r.stream.Handle(conn) + err := r.Stream.Handle(conn) if err != nil && !errors.Is(err, context.Canceled) { E.LogError("handle connection error", err, &r.l) connTask.Finish(err) diff --git a/internal/route/stream_impl.go b/internal/route/stream_impl.go index 6b29dc9a..68df2f50 100644 --- a/internal/route/stream_impl.go +++ b/internal/route/stream_impl.go @@ -8,7 +8,7 @@ import ( "time" "github.com/yusing/go-proxy/internal/net/types" - T "github.com/yusing/go-proxy/internal/proxy/fields" + T "github.com/yusing/go-proxy/internal/route/types" U "github.com/yusing/go-proxy/internal/utils" ) diff --git a/internal/proxy/fields/alias.go b/internal/route/types/alias.go similarity index 55% rename from internal/proxy/fields/alias.go rename to internal/route/types/alias.go index 07a91eb8..9201ee34 100644 --- a/internal/proxy/fields/alias.go +++ b/internal/route/types/alias.go @@ -1,3 +1,3 @@ -package fields +package types type Alias string diff --git a/internal/route/types/entry.go b/internal/route/types/entry.go new file mode 100644 index 00000000..ae907ded --- /dev/null +++ b/internal/route/types/entry.go @@ -0,0 +1,17 @@ +package types + +import ( + idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" + loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types" + net "github.com/yusing/go-proxy/internal/net/types" + "github.com/yusing/go-proxy/internal/watcher/health" +) + +type Entry interface { + TargetName() string + TargetURL() net.URL + RawEntry() *RawEntry + LoadBalanceConfig() *loadbalance.Config + HealthCheckConfig() *health.HealthCheckConfig + IdlewatcherConfig() *idlewatcher.Config +} diff --git a/internal/proxy/fields/headers.go b/internal/route/types/headers.go similarity index 95% rename from internal/proxy/fields/headers.go rename to internal/route/types/headers.go index 109eb44c..b2af83a2 100644 --- a/internal/proxy/fields/headers.go +++ b/internal/route/types/headers.go @@ -1,4 +1,4 @@ -package fields +package types import ( "net/http" diff --git a/internal/proxy/fields/host.go b/internal/route/types/host.go similarity index 89% rename from internal/proxy/fields/host.go rename to internal/route/types/host.go index 892e72cc..c1647bc3 100644 --- a/internal/proxy/fields/host.go +++ b/internal/route/types/host.go @@ -1,4 +1,4 @@ -package fields +package types type ( Host string diff --git a/internal/proxy/fields/path_pattern.go b/internal/route/types/path_pattern.go similarity index 98% rename from internal/proxy/fields/path_pattern.go rename to internal/route/types/path_pattern.go index 0d6df1c5..b1f412df 100644 --- a/internal/proxy/fields/path_pattern.go +++ b/internal/route/types/path_pattern.go @@ -1,4 +1,4 @@ -package fields +package types import ( "errors" diff --git a/internal/proxy/fields/path_pattern_test.go b/internal/route/types/path_pattern_test.go similarity index 98% rename from internal/proxy/fields/path_pattern_test.go rename to internal/route/types/path_pattern_test.go index 261ee3ff..0285e7e1 100644 --- a/internal/proxy/fields/path_pattern_test.go +++ b/internal/route/types/path_pattern_test.go @@ -1,4 +1,4 @@ -package fields +package types import ( "errors" diff --git a/internal/proxy/fields/port.go b/internal/route/types/port.go similarity index 98% rename from internal/proxy/fields/port.go rename to internal/route/types/port.go index 9b809fda..dd433aa9 100644 --- a/internal/proxy/fields/port.go +++ b/internal/route/types/port.go @@ -1,4 +1,4 @@ -package fields +package types import ( "strconv" diff --git a/internal/proxy/entry/raw.go b/internal/route/types/raw_entry.go similarity index 96% rename from internal/proxy/entry/raw.go rename to internal/route/types/raw_entry.go index 0f0836e3..0183f889 100644 --- a/internal/proxy/entry/raw.go +++ b/internal/route/types/raw_entry.go @@ -1,4 +1,4 @@ -package entry +package types import ( "strconv" @@ -9,7 +9,7 @@ import ( "github.com/yusing/go-proxy/internal/docker" "github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/logging" - "github.com/yusing/go-proxy/internal/net/http/loadbalancer" + loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types" U "github.com/yusing/go-proxy/internal/utils" F "github.com/yusing/go-proxy/internal/utils/functional" "github.com/yusing/go-proxy/internal/utils/strutils" @@ -29,7 +29,7 @@ type ( NoTLSVerify bool `json:"no_tls_verify,omitempty" yaml:"no_tls_verify"` // https proxy only PathPatterns []string `json:"path_patterns,omitempty" yaml:"path_patterns"` // http(s) proxy only HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty" yaml:"healthcheck"` - LoadBalance *loadbalancer.Config `json:"load_balance,omitempty" yaml:"load_balance"` + LoadBalance *loadbalance.Config `json:"load_balance,omitempty" yaml:"load_balance"` Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty" yaml:"middlewares"` Homepage *homepage.Item `json:"homepage,omitempty" yaml:"homepage"` diff --git a/internal/route/types/route.go b/internal/route/types/route.go new file mode 100644 index 00000000..d44a812e --- /dev/null +++ b/internal/route/types/route.go @@ -0,0 +1,18 @@ +package types + +import ( + "net/http" + + net "github.com/yusing/go-proxy/internal/net/types" +) + +type ( + HTTPRoute interface { + Entry + http.Handler + } + StreamRoute interface { + Entry + net.Stream + } +) diff --git a/internal/proxy/fields/scheme.go b/internal/route/types/scheme.go similarity index 97% rename from internal/proxy/fields/scheme.go rename to internal/route/types/scheme.go index b0064642..b2266c86 100644 --- a/internal/proxy/fields/scheme.go +++ b/internal/route/types/scheme.go @@ -1,4 +1,4 @@ -package fields +package types import ( E "github.com/yusing/go-proxy/internal/error" diff --git a/internal/proxy/fields/stream_port.go b/internal/route/types/stream_port.go similarity index 97% rename from internal/proxy/fields/stream_port.go rename to internal/route/types/stream_port.go index 6cbc7678..3c1283cd 100644 --- a/internal/proxy/fields/stream_port.go +++ b/internal/route/types/stream_port.go @@ -1,4 +1,4 @@ -package fields +package types import ( "strings" diff --git a/internal/proxy/fields/stream_port_test.go b/internal/route/types/stream_port_test.go similarity index 98% rename from internal/proxy/fields/stream_port_test.go rename to internal/route/types/stream_port_test.go index a18732a2..61547490 100644 --- a/internal/proxy/fields/stream_port_test.go +++ b/internal/route/types/stream_port_test.go @@ -1,4 +1,4 @@ -package fields +package types import ( "strconv" diff --git a/internal/proxy/fields/stream_scheme.go b/internal/route/types/stream_scheme.go similarity index 98% rename from internal/proxy/fields/stream_scheme.go rename to internal/route/types/stream_scheme.go index 0c0180eb..968bf453 100644 --- a/internal/proxy/fields/stream_scheme.go +++ b/internal/route/types/stream_scheme.go @@ -1,4 +1,4 @@ -package fields +package types import ( "fmt" diff --git a/internal/proxy/fields/stream_scheme_test.go b/internal/route/types/stream_scheme_test.go similarity index 97% rename from internal/proxy/fields/stream_scheme_test.go rename to internal/route/types/stream_scheme_test.go index 15227c04..43f80105 100644 --- a/internal/proxy/fields/stream_scheme_test.go +++ b/internal/route/types/stream_scheme_test.go @@ -1,4 +1,4 @@ -package fields +package types import ( "testing"