modules reorganized and code refactor

This commit is contained in:
yusing 2024-11-25 01:40:12 +08:00
parent f3b21e6bd9
commit d723403b6b
46 changed files with 437 additions and 331 deletions

View file

@ -14,12 +14,12 @@ import (
"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/metrics"
"github.com/yusing/go-proxy/internal/net/http/middleware" "github.com/yusing/go-proxy/internal/net/http/middleware"
R "github.com/yusing/go-proxy/internal/route" "github.com/yusing/go-proxy/internal/net/http/server"
"github.com/yusing/go-proxy/internal/server"
"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"
) )
@ -136,7 +136,7 @@ func main() {
CertProvider: autocert, CertProvider: autocert,
HTTPAddr: common.ProxyHTTPAddr, HTTPAddr: common.ProxyHTTPAddr,
HTTPSAddr: common.ProxyHTTPSAddr, HTTPSAddr: common.ProxyHTTPSAddr,
Handler: http.HandlerFunc(R.ProxyHandler), Handler: http.HandlerFunc(entrypoint.Handler),
RedirectToHTTPS: config.Value().RedirectToHTTPS, RedirectToHTTPS: config.Value().RedirectToHTTPS,
}) })
server.StartServer(server.Options{ server.StartServer(server.Options{

View file

@ -10,10 +10,10 @@ import (
"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"
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/notif" "github.com/yusing/go-proxy/internal/notif"
"github.com/yusing/go-proxy/internal/route"
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"
U "github.com/yusing/go-proxy/internal/utils" U "github.com/yusing/go-proxy/internal/utils"
@ -183,7 +183,7 @@ func (cfg *Config) load() E.Error {
model.MatchDomains[i] = "." + domain model.MatchDomains[i] = "." + domain
} }
} }
route.SetFindMuxDomains(model.MatchDomains) entrypoint.SetFindRouteDomains(model.MatchDomains)
return errs.Error() return errs.Error()
} }

View file

@ -6,14 +6,16 @@ import (
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/proxy/entry" route "github.com/yusing/go-proxy/internal/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" 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" "github.com/yusing/go-proxy/internal/utils/strutils"
) )
func DumpEntries() map[string]*entry.RawEntry { func DumpEntries() map[string]*types.RawEntry {
entries := make(map[string]*entry.RawEntry) entries := make(map[string]*types.RawEntry)
instance.providers.RangeAll(func(_ string, p *proxy.Provider) { instance.providers.RangeAll(func(_ string, p *proxy.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
@ -43,8 +45,8 @@ func HomepageConfig() homepage.Config {
} }
hpCfg := homepage.NewHomePageConfig() hpCfg := homepage.NewHomePageConfig()
route.GetReverseProxies().RangeAll(func(alias string, r *route.HTTPRoute) { routes.GetHTTPRoutes().RangeAll(func(alias string, r types.HTTPRoute) {
en := r.Raw en := r.RawEntry()
item := en.Homepage item := en.Homepage
if item == nil { if item == nil {
item = new(homepage.Item) item = new(homepage.Item)
@ -113,23 +115,23 @@ func HomepageConfig() homepage.Config {
} }
func RoutesByAlias(typeFilter ...route.RouteType) map[string]any { func RoutesByAlias(typeFilter ...route.RouteType) map[string]any {
routes := make(map[string]any) rts := make(map[string]any)
if len(typeFilter) == 0 || typeFilter[0] == "" { if len(typeFilter) == 0 || typeFilter[0] == "" {
typeFilter = []route.RouteType{route.RouteTypeReverseProxy, route.RouteTypeStream} typeFilter = []route.RouteType{route.RouteTypeReverseProxy, route.RouteTypeStream}
} }
for _, t := range typeFilter { for _, t := range typeFilter {
switch t { switch t {
case route.RouteTypeReverseProxy: case route.RouteTypeReverseProxy:
route.GetReverseProxies().RangeAll(func(alias string, r *route.HTTPRoute) { routes.GetHTTPRoutes().RangeAll(func(alias string, r types.HTTPRoute) {
routes[alias] = r rts[alias] = r
}) })
case route.RouteTypeStream: case route.RouteTypeStream:
route.GetStreamProxies().RangeAll(func(alias string, r *route.StreamRoute) { routes.GetStreamRoutes().RangeAll(func(alias string, r types.StreamRoute) {
routes[alias] = r rts[alias] = r
}) })
} }
} }
return routes return rts
} }
func Statistics() map[string]any { func Statistics() map[string]any {

View file

@ -6,19 +6,21 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/yusing/go-proxy/internal/common" "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" E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/metrics" "github.com/yusing/go-proxy/internal/metrics"
gphttp "github.com/yusing/go-proxy/internal/net/http" gphttp "github.com/yusing/go-proxy/internal/net/http"
net "github.com/yusing/go-proxy/internal/net/types" 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" "github.com/yusing/go-proxy/internal/task"
U "github.com/yusing/go-proxy/internal/utils" U "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/watcher/health" "github.com/yusing/go-proxy/internal/watcher/health"
"github.com/yusing/go-proxy/internal/watcher/health/monitor" "github.com/yusing/go-proxy/internal/watcher/health/monitor"
) )
type waker struct { type (
Waker = types.Waker
waker struct {
_ U.NoCopy _ U.NoCopy
rp *gphttp.ReverseProxy rp *gphttp.ReverseProxy
@ -27,7 +29,8 @@ type waker struct {
metric *metrics.Gauge metric *metrics.Gauge
ready atomic.Bool ready atomic.Bool
} }
)
const ( const (
idleWakerCheckInterval = 100 * time.Millisecond idleWakerCheckInterval = 100 * time.Millisecond
@ -36,7 +39,7 @@ const (
// TODO: support stream // 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 := entry.HealthCheckConfig()
hcCfg.Timeout = idleWakerCheckTimeout hcCfg.Timeout = idleWakerCheckTimeout
@ -69,11 +72,11 @@ func newWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReversePr
} }
// lifetime should follow route provider. // 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) 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) return newWaker(providerSubTask, entry, nil, stream)
} }

View file

@ -12,7 +12,7 @@ import (
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
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/proxy/entry" route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
U "github.com/yusing/go-proxy/internal/utils" U "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"
@ -49,7 +49,7 @@ var (
const dockerReqTimeout = 3 * time.Second 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() cfg := entry.IdlewatcherConfig()
if cfg.IdleTimeout == 0 { if cfg.IdleTimeout == 0 {

View file

@ -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)
}
}

View file

@ -0,0 +1,7 @@
package entrypoint
import (
"github.com/yusing/go-proxy/internal/logging"
)
var logger = logging.With().Str("module", "entrypoint").Logger()

View file

@ -14,7 +14,7 @@ type ipHash struct {
*LoadBalancer *LoadBalancer
realIP *middleware.Middleware realIP *middleware.Middleware
pool servers pool Servers
mu sync.Mutex mu sync.Mutex
} }
@ -26,7 +26,7 @@ func (lb *LoadBalancer) newIPHash() impl {
var err E.Error var err E.Error
impl.realIP, err = middleware.NewRealIP(lb.Options) impl.realIP, err = middleware.NewRealIP(lb.Options)
if err != nil { 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 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 { if impl.realIP != nil {
impl.realIP.ModifyRequest(impl.serveHTTP, rw, r) impl.realIP.ModifyRequest(impl.serveHTTP, rw, r)
} else { } else {
@ -72,7 +72,7 @@ func (impl *ipHash) serveHTTP(rw http.ResponseWriter, r *http.Request) {
ip, _, err := net.SplitHostPort(r.RemoteAddr) ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil { if err != nil {
http.Error(rw, "Internal error", http.StatusInternalServerError) 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 return
} }
idx := hashIP(ip) % uint32(len(impl.pool)) idx := hashIP(ip) % uint32(len(impl.pool))

View file

@ -27,18 +27,18 @@ func (impl *leastConn) OnRemoveServer(srv *Server) {
impl.nConn.Delete(srv) 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] srv := srvs[0]
minConn, ok := impl.nConn.Load(srv) minConn, ok := impl.nConn.Load(srv)
if !ok { 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) http.Error(rw, "Internal error", http.StatusInternalServerError)
} }
for i := 1; i < len(srvs); i++ { for i := 1; i < len(srvs); i++ {
nConn, ok := impl.nConn.Load(srvs[i]) nConn, ok := impl.nConn.Load(srvs[i])
if !ok { 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) http.Error(rw, "Internal error", http.StatusInternalServerError)
} }
if nConn.Load() < minConn.Load() { if nConn.Load() < minConn.Load() {

View file

@ -7,9 +7,8 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/common" "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" 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/task"
"github.com/yusing/go-proxy/internal/watcher/health" "github.com/yusing/go-proxy/internal/watcher/health"
"github.com/yusing/go-proxy/internal/watcher/health/monitor" "github.com/yusing/go-proxy/internal/watcher/health/monitor"
@ -19,19 +18,12 @@ import (
// TODO: support weighted mode. // TODO: support weighted mode.
type ( type (
impl interface { impl interface {
ServeHTTP(srvs servers, rw http.ResponseWriter, r *http.Request) ServeHTTP(srvs Servers, rw http.ResponseWriter, r *http.Request)
OnAddServer(srv *Server) OnAddServer(srv *Server)
OnRemoveServer(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 impl
*Config *Config
@ -40,20 +32,20 @@ type (
pool Pool pool Pool
poolMu sync.Mutex poolMu sync.Mutex
sumWeight weightType sumWeight Weight
startTime time.Time startTime time.Time
}
weightType uint16 l zerolog.Logger
}
) )
const maxWeight weightType = 100 const maxWeight Weight = 100
func New(cfg *Config) *LoadBalancer { func New(cfg *Config) *LoadBalancer {
lb := &LoadBalancer{ lb := &LoadBalancer{
Logger: logger.With().Str("name", cfg.Link).Logger(),
Config: new(Config), Config: new(Config),
pool: newPool(), pool: types.NewServerPool(),
l: logger.With().Str("name", cfg.Link).Logger(),
} }
lb.UpdateConfigIfNeeded(cfg) lb.UpdateConfigIfNeeded(cfg)
return lb return lb
@ -81,11 +73,11 @@ func (lb *LoadBalancer) Finish(reason any) {
func (lb *LoadBalancer) updateImpl() { func (lb *LoadBalancer) updateImpl() {
switch lb.Mode { switch lb.Mode {
case Unset, RoundRobin: case types.ModeUnset, types.ModeRoundRobin:
lb.impl = lb.newRoundRobin() lb.impl = lb.newRoundRobin()
case LeastConn: case types.ModeLeastConn:
lb.impl = lb.newLeastConn() lb.impl = lb.newLeastConn()
case IPHash: case types.ModeIPHash:
lb.impl = lb.newIPHash() lb.impl = lb.newIPHash()
default: // should happen in test only default: // should happen in test only
lb.impl = lb.newRoundRobin() lb.impl = lb.newRoundRobin()
@ -102,10 +94,10 @@ func (lb *LoadBalancer) UpdateConfigIfNeeded(cfg *Config) {
lb.Link = cfg.Link lb.Link = cfg.Link
if lb.Mode == Unset && cfg.Mode != Unset { if lb.Mode == types.ModeUnset && cfg.Mode != types.ModeUnset {
lb.Mode = cfg.Mode lb.Mode = cfg.Mode
if !lb.Mode.ValidateUpdate() { 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() lb.updateImpl()
} }
@ -135,7 +127,7 @@ func (lb *LoadBalancer) AddServer(srv *Server) {
lb.rebalance() lb.rebalance()
lb.impl.OnAddServer(srv) lb.impl.OnAddServer(srv)
lb.Debug(). lb.l.Debug().
Str("action", "add"). Str("action", "add").
Str("server", srv.Name). Str("server", srv.Name).
Msgf("%d servers available", lb.pool.Size()) Msgf("%d servers available", lb.pool.Size())
@ -155,7 +147,7 @@ func (lb *LoadBalancer) RemoveServer(srv *Server) {
lb.rebalance() lb.rebalance()
lb.impl.OnRemoveServer(srv) lb.impl.OnRemoveServer(srv)
lb.Debug(). lb.l.Debug().
Str("action", "remove"). Str("action", "remove").
Str("server", srv.Name). Str("server", srv.Name).
Msgf("%d servers left", lb.pool.Size()) Msgf("%d servers left", lb.pool.Size())
@ -174,8 +166,8 @@ func (lb *LoadBalancer) rebalance() {
return return
} }
if lb.sumWeight == 0 { // distribute evenly if lb.sumWeight == 0 { // distribute evenly
weightEach := maxWeight / weightType(lb.pool.Size()) weightEach := maxWeight / Weight(lb.pool.Size())
remainder := maxWeight % weightType(lb.pool.Size()) remainder := maxWeight % Weight(lb.pool.Size())
lb.pool.RangeAll(func(_ string, s *Server) { lb.pool.RangeAll(func(_ string, s *Server) {
s.Weight = weightEach s.Weight = weightEach
lb.sumWeight += weightEach lb.sumWeight += weightEach
@ -192,7 +184,7 @@ func (lb *LoadBalancer) rebalance() {
lb.sumWeight = 0 lb.sumWeight = 0
lb.pool.RangeAll(func(_ string, s *Server) { 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 lb.sumWeight += s.Weight
}) })
@ -226,13 +218,7 @@ func (lb *LoadBalancer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
if r.Header.Get(common.HeaderCheckRedirect) != "" { if r.Header.Get(common.HeaderCheckRedirect) != "" {
// wake all servers // wake all servers
for _, srv := range srvs { for _, srv := range srvs {
// wake only if server implements Waker srv.TryWake()
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)
}
}
} }
} }
lb.impl.ServeHTTP(srvs, rw, r) lb.impl.ServeHTTP(srvs, rw, r)
@ -246,7 +232,7 @@ func (lb *LoadBalancer) Uptime() time.Duration {
func (lb *LoadBalancer) MarshalJSON() ([]byte, error) { func (lb *LoadBalancer) MarshalJSON() ([]byte, error) {
extra := make(map[string]any) extra := make(map[string]any)
lb.pool.RangeAll(func(k string, v *Server) { lb.pool.RangeAll(func(k string, v *Server) {
extra[v.Name] = v.healthMon extra[v.Name] = v.HealthMonitor()
}) })
return (&monitor.JSONRepresentation{ return (&monitor.JSONRepresentation{

View file

@ -3,13 +3,14 @@ package loadbalancer
import ( import (
"testing" "testing"
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"
) )
func TestRebalance(t *testing.T) { func TestRebalance(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("zero", func(t *testing.T) { t.Run("zero", func(t *testing.T) {
lb := New(new(Config)) lb := New(new(loadbalance.Config))
for range 10 { for range 10 {
lb.AddServer(&Server{}) lb.AddServer(&Server{})
} }
@ -17,25 +18,25 @@ func TestRebalance(t *testing.T) {
ExpectEqual(t, lb.sumWeight, maxWeight) ExpectEqual(t, lb.sumWeight, maxWeight)
}) })
t.Run("less", func(t *testing.T) { t.Run("less", func(t *testing.T) {
lb := New(new(Config)) lb := New(new(loadbalance.Config))
lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .1)}) lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .1)})
lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .2)}) lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .2)})
lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .3)}) lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .3)})
lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .2)}) lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .2)})
lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .1)}) lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .1)})
lb.rebalance() lb.rebalance()
// t.Logf("%s", U.Must(json.MarshalIndent(lb.pool, "", " "))) // t.Logf("%s", U.Must(json.MarshalIndent(lb.pool, "", " ")))
ExpectEqual(t, lb.sumWeight, maxWeight) ExpectEqual(t, lb.sumWeight, maxWeight)
}) })
t.Run("more", func(t *testing.T) { t.Run("more", func(t *testing.T) {
lb := New(new(Config)) lb := New(new(loadbalance.Config))
lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .1)}) lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .1)})
lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .2)}) lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .2)})
lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .3)}) lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .3)})
lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .4)}) lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .4)})
lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .3)}) lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .3)})
lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .2)}) lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .2)})
lb.AddServer(&Server{Weight: weightType(float64(maxWeight) * .1)}) lb.AddServer(&Server{Weight: loadbalance.Weight(float64(maxWeight) * .1)})
lb.rebalance() lb.rebalance()
// t.Logf("%s", U.Must(json.MarshalIndent(lb.pool, "", " "))) // t.Logf("%s", U.Must(json.MarshalIndent(lb.pool, "", " ")))
ExpectEqual(t, lb.sumWeight, maxWeight) ExpectEqual(t, lb.sumWeight, maxWeight)

View file

@ -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
}

View file

@ -13,7 +13,7 @@ func (*LoadBalancer) newRoundRobin() impl { return &roundRobin{} }
func (lb *roundRobin) OnAddServer(srv *Server) {} func (lb *roundRobin) OnAddServer(srv *Server) {}
func (lb *roundRobin) OnRemoveServer(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)) index := lb.index.Add(1) % uint32(len(srvs))
srvs[index].ServeHTTP(rw, r) srvs[index].ServeHTTP(rw, r)
if lb.index.Load() >= 2*uint32(len(srvs)) { if lb.index.Load() >= 2*uint32(len(srvs)) {

View file

@ -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
)

View file

@ -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"`
}

View file

@ -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
}

View file

@ -1,9 +1,10 @@
package loadbalancer package types
import ( import (
"net/http" "net/http"
"time" "time"
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
"github.com/yusing/go-proxy/internal/net/types" "github.com/yusing/go-proxy/internal/net/types"
U "github.com/yusing/go-proxy/internal/utils" U "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,18 +17,18 @@ type (
Name string Name string
URL types.URL URL types.URL
Weight weightType Weight Weight
handler http.Handler handler http.Handler
healthMon health.HealthMonitor healthMon health.HealthMonitor
} }
servers = []*Server Servers = []*Server
Pool = F.Map[string, *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{ srv := &Server{
Name: name, Name: name,
URL: url, URL: url,
@ -53,3 +54,17 @@ func (srv *Server) Status() health.Status {
func (srv *Server) Uptime() time.Duration { func (srv *Server) Uptime() time.Duration {
return srv.healthMon.Uptime() 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
}

View file

@ -0,0 +1,3 @@
package types
type Weight uint16

View file

@ -1,25 +1,14 @@
package entry package entry
import ( import (
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/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/loadbalancer" route "github.com/yusing/go-proxy/internal/route/types"
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"
) )
type Entry interface { type Entry = route.Entry
TargetName() string
TargetURL() net.URL
RawEntry() *RawEntry
LoadBalanceConfig() *loadbalancer.Config
HealthCheckConfig() *health.HealthCheckConfig
IdlewatcherConfig() *idlewatcher.Config
}
func ValidateEntry(m *RawEntry) (Entry, E.Error) { func ValidateEntry(m *route.RawEntry) (Entry, E.Error) {
scheme, err := T.NewScheme(m.Scheme) scheme, err := route.NewScheme(m.Scheme)
if err != nil { if err != nil {
return nil, E.From(err) return nil, E.From(err)
} }

View file

@ -7,22 +7,22 @@ import (
"github.com/yusing/go-proxy/internal/docker" "github.com/yusing/go-proxy/internal/docker"
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/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/loadbalancer" loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
net "github.com/yusing/go-proxy/internal/net/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" "github.com/yusing/go-proxy/internal/watcher/health"
) )
type ReverseProxyEntry struct { // real model after validation type ReverseProxyEntry struct { // real model after validation
Raw *RawEntry `json:"raw"` Raw *route.RawEntry `json:"raw"`
Alias fields.Alias `json:"alias"` Alias route.Alias `json:"alias"`
Scheme fields.Scheme `json:"scheme"` Scheme route.Scheme `json:"scheme"`
URL net.URL `json:"url"` URL net.URL `json:"url"`
NoTLSVerify bool `json:"no_tls_verify,omitempty"` 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"` 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"` Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty"`
/* Docker only */ /* Docker only */
@ -37,11 +37,11 @@ func (rp *ReverseProxyEntry) TargetURL() net.URL {
return rp.URL return rp.URL
} }
func (rp *ReverseProxyEntry) RawEntry() *RawEntry { func (rp *ReverseProxyEntry) RawEntry() *route.RawEntry {
return rp.Raw return rp.Raw
} }
func (rp *ReverseProxyEntry) LoadBalanceConfig() *loadbalancer.Config { func (rp *ReverseProxyEntry) LoadBalanceConfig() *loadbalance.Config {
return rp.LoadBalance return rp.LoadBalance
} }
@ -53,7 +53,7 @@ func (rp *ReverseProxyEntry) IdlewatcherConfig() *idlewatcher.Config {
return rp.Idlewatcher 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 cont := m.Container
if cont == nil { if cont == nil {
cont = docker.DummyContainer cont = docker.DummyContainer
@ -64,9 +64,9 @@ func validateRPEntry(m *RawEntry, s fields.Scheme, errs *E.Builder) *ReverseProx
lb = nil lb = nil
} }
host := E.Collect(errs, fields.ValidateHost, m.Host) host := E.Collect(errs, route.ValidateHost, m.Host)
port := E.Collect(errs, fields.ValidatePort, m.Port) port := E.Collect(errs, route.ValidatePort, m.Port)
pathPats := E.Collect(errs, fields.ValidatePathPatterns, m.PathPatterns) pathPats := E.Collect(errs, route.ValidatePathPatterns, m.PathPatterns)
url := E.Collect(errs, url.Parse, fmt.Sprintf("%s://%s:%d", s, host, port)) url := E.Collect(errs, url.Parse, fmt.Sprintf("%s://%s:%d", s, host, port))
iwCfg := E.Collect(errs, idlewatcher.ValidateConfig, cont) iwCfg := E.Collect(errs, idlewatcher.ValidateConfig, cont)
@ -76,7 +76,7 @@ func validateRPEntry(m *RawEntry, s fields.Scheme, errs *E.Builder) *ReverseProx
return &ReverseProxyEntry{ return &ReverseProxyEntry{
Raw: m, Raw: m,
Alias: fields.Alias(m.Alias), Alias: route.Alias(m.Alias),
Scheme: s, Scheme: s,
URL: net.NewURL(url), URL: net.NewURL(url),
NoTLSVerify: m.NoTLSVerify, NoTLSVerify: m.NoTLSVerify,

View file

@ -6,20 +6,20 @@ import (
"github.com/yusing/go-proxy/internal/docker" "github.com/yusing/go-proxy/internal/docker"
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/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/loadbalancer" loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
net "github.com/yusing/go-proxy/internal/net/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" "github.com/yusing/go-proxy/internal/watcher/health"
) )
type StreamEntry struct { type StreamEntry struct {
Raw *RawEntry `json:"raw"` Raw *route.RawEntry `json:"raw"`
Alias fields.Alias `json:"alias"` Alias route.Alias `json:"alias"`
Scheme fields.StreamScheme `json:"scheme"` Scheme route.StreamScheme `json:"scheme"`
URL net.URL `json:"url"` URL net.URL `json:"url"`
Host fields.Host `json:"host,omitempty"` Host route.Host `json:"host,omitempty"`
Port fields.StreamPort `json:"port,omitempty"` Port route.StreamPort `json:"port,omitempty"`
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"` HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
/* Docker only */ /* Docker only */
@ -34,11 +34,11 @@ func (s *StreamEntry) TargetURL() net.URL {
return s.URL return s.URL
} }
func (s *StreamEntry) RawEntry() *RawEntry { func (s *StreamEntry) RawEntry() *route.RawEntry {
return s.Raw return s.Raw
} }
func (s *StreamEntry) LoadBalanceConfig() *loadbalancer.Config { func (s *StreamEntry) LoadBalanceConfig() *loadbalance.Config {
// TODO: support stream load balance // TODO: support stream load balance
return nil return nil
} }
@ -51,15 +51,15 @@ func (s *StreamEntry) IdlewatcherConfig() *idlewatcher.Config {
return s.Idlewatcher return s.Idlewatcher
} }
func validateStreamEntry(m *RawEntry, errs *E.Builder) *StreamEntry { func validateStreamEntry(m *route.RawEntry, errs *E.Builder) *StreamEntry {
cont := m.Container cont := m.Container
if cont == nil { if cont == nil {
cont = docker.DummyContainer cont = docker.DummyContainer
} }
host := E.Collect(errs, fields.ValidateHost, m.Host) host := E.Collect(errs, route.ValidateHost, m.Host)
port := E.Collect(errs, fields.ValidateStreamPort, m.Port) port := E.Collect(errs, route.ValidateStreamPort, m.Port)
scheme := E.Collect(errs, fields.ValidateStreamScheme, m.Scheme) scheme := E.Collect(errs, route.ValidateStreamScheme, m.Scheme)
url := E.Collect(errs, net.ParseURL, fmt.Sprintf("%s://%s:%d", scheme.ListeningScheme, host, port.ProxyPort)) url := E.Collect(errs, net.ParseURL, fmt.Sprintf("%s://%s:%d", scheme.ListeningScheme, host, port.ProxyPort))
idleWatcherCfg := E.Collect(errs, idlewatcher.ValidateConfig, cont) idleWatcherCfg := E.Collect(errs, idlewatcher.ValidateConfig, cont)
@ -69,7 +69,7 @@ func validateStreamEntry(m *RawEntry, errs *E.Builder) *StreamEntry {
return &StreamEntry{ return &StreamEntry{
Raw: m, Raw: m,
Alias: fields.Alias(m.Alias), Alias: route.Alias(m.Alias),
Scheme: *scheme, Scheme: *scheme,
URL: url, URL: url,
Host: host, Host: host,

View file

@ -1,10 +1,7 @@
package route package route
import ( import (
"errors"
"fmt"
"net/http" "net/http"
"strings"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
@ -12,12 +9,12 @@ import (
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
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/loadbalancer" "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"
"github.com/yusing/go-proxy/internal/net/http/middleware/errorpage" "github.com/yusing/go-proxy/internal/route/entry"
"github.com/yusing/go-proxy/internal/proxy/entry" "github.com/yusing/go-proxy/internal/route/routes"
PT "github.com/yusing/go-proxy/internal/proxy/fields" route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/task" "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"
"github.com/yusing/go-proxy/internal/watcher/health/monitor" "github.com/yusing/go-proxy/internal/watcher/health/monitor"
) )
@ -38,27 +35,10 @@ type (
l zerolog.Logger l zerolog.Logger
} }
SubdomainKey = PT.Alias SubdomainKey = route.Alias
) )
var ( // var globalMux = http.NewServeMux() // TODO: support regex subdomain matching.
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)
}
}
func NewHTTPRoute(entry *entry.ReverseProxyEntry) (impl, E.Error) { func NewHTTPRoute(entry *entry.ReverseProxyEntry) (impl, E.Error) {
var trans *http.Transport var trans *http.Transport
@ -141,9 +121,9 @@ func (r *HTTPRoute) Start(providerSubtask task.Task) E.Error {
if entry.UseLoadBalance(r) { if entry.UseLoadBalance(r) {
r.addToLoadBalancer() r.addToLoadBalancer()
} else { } else {
httpRoutes.Store(string(r.Alias), r) routes.SetHTTPRoute(string(r.Alias), r)
r.task.OnFinished("remove from route table", func() { 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() { func (r *HTTPRoute) addToLoadBalancer() {
var lb *loadbalancer.LoadBalancer var lb *loadbalancer.LoadBalancer
linked, ok := httpRoutes.Load(r.LoadBalance.Link) l, ok := routes.GetHTTPRoute(r.LoadBalance.Link)
linked := l.(*HTTPRoute)
if ok { if ok {
lb = linked.loadBalancer lb = linked.loadBalancer
lb.UpdateConfigIfNeeded(r.LoadBalance) lb.UpdateConfigIfNeeded(r.LoadBalance)
@ -175,96 +156,26 @@ func (r *HTTPRoute) addToLoadBalancer() {
lb = loadbalancer.New(r.LoadBalance) lb = loadbalancer.New(r.LoadBalance)
lbTask := r.task.Parent().Subtask("loadbalancer " + r.LoadBalance.Link) lbTask := r.task.Parent().Subtask("loadbalancer " + r.LoadBalance.Link)
lbTask.OnCancel("remove lb from routes", func() { lbTask.OnCancel("remove lb from routes", func() {
httpRoutes.Delete(r.LoadBalance.Link) routes.DeleteHTTPRoute(r.LoadBalance.Link)
}) })
lb.Start(lbTask) lb.Start(lbTask)
linked = &HTTPRoute{ linked = &HTTPRoute{
ReverseProxyEntry: &entry.ReverseProxyEntry{ ReverseProxyEntry: &entry.ReverseProxyEntry{
Raw: &entry.RawEntry{ Raw: &route.RawEntry{
Homepage: r.Raw.Homepage, Homepage: r.Raw.Homepage,
}, },
Alias: PT.Alias(lb.Link), Alias: route.Alias(lb.Link),
}, },
HealthMon: lb, HealthMon: lb,
loadBalancer: lb, loadBalancer: lb,
handler: lb, handler: lb,
} }
httpRoutes.Store(r.LoadBalance.Link, linked) routes.SetHTTPRoute(r.LoadBalance.Link, linked)
} }
r.loadBalancer = lb 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) lb.AddServer(r.server)
r.task.OnCancel("remove server from lb", func() { r.task.OnCancel("remove server from lb", func() {
lb.RemoveServer(r.server) 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)
}
}

View file

@ -9,7 +9,6 @@ import (
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/docker" "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error" 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"
U "github.com/yusing/go-proxy/internal/utils" U "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils" "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) { func (p *DockerProvider) loadRoutesImpl() (route.Routes, E.Error) {
routes := route.NewRoutes() routes := route.NewRoutes()
entries := entry.NewProxyEntries() entries := route.NewProxyEntries()
containers, err := docker.ListContainers(p.dockerHost) containers, err := docker.ListContainers(p.dockerHost)
if err != nil { if err != nil {
@ -78,7 +77,7 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, E.Error) {
// there may be some valid entries in `en` // there may be some valid entries in `en`
dups := entries.MergeFrom(newEntries) dups := entries.MergeFrom(newEntries)
// add the duplicate proxy entries to the error // 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) 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. // Returns a list of proxy entries for a container.
// Always non-nil. // Always non-nil.
func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container) (entries entry.RawEntries, _ E.Error) { func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container) (entries route.RawEntries, _ E.Error) {
entries = entry.NewProxyEntries() entries = route.NewProxyEntries()
if p.shouldIgnore(container) { if p.shouldIgnore(container) {
return return
@ -107,7 +106,7 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container)
// init entries map for all aliases // init entries map for all aliases
for _, a := range container.Aliases { for _, a := range container.Aliases {
entries.Store(a, &entry.RawEntry{ entries.Store(a, &route.RawEntry{
Alias: a, Alias: a,
Container: container, Container: container,
}) })
@ -154,9 +153,9 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container)
} }
// init entry if not exist // init entry if not exist
var en *entry.RawEntry var en *route.RawEntry
if en, ok = entries.Load(alias); !ok { if en, ok = entries.Load(alias); !ok {
en = &entry.RawEntry{ en = &route.RawEntry{
Alias: alias, Alias: alias,
Container: container, Container: container,
} }
@ -172,7 +171,7 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container)
} }
} }
if wildcardProps != nil { 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 { if err := U.Deserialize(wildcardProps, re); err != nil {
errs.Add(err.Subject(alias)) errs.Add(err.Subject(alias))
} }

View file

@ -10,8 +10,8 @@ import (
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
D "github.com/yusing/go-proxy/internal/docker" D "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/proxy/entry" "github.com/yusing/go-proxy/internal/route/entry"
T "github.com/yusing/go-proxy/internal/proxy/fields" T "github.com/yusing/go-proxy/internal/route/types"
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"
) )

View file

@ -3,8 +3,8 @@ package provider
import ( import (
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error" 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"
"github.com/yusing/go-proxy/internal/route/entry"
"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"
) )

View file

@ -7,8 +7,7 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/proxy/entry" "github.com/yusing/go-proxy/internal/route"
R "github.com/yusing/go-proxy/internal/route"
U "github.com/yusing/go-proxy/internal/utils" U "github.com/yusing/go-proxy/internal/utils"
W "github.com/yusing/go-proxy/internal/watcher" W "github.com/yusing/go-proxy/internal/watcher"
) )
@ -44,9 +43,9 @@ func (p *FileProvider) Logger() *zerolog.Logger {
return &p.l return &p.l
} }
func (p *FileProvider) loadRoutesImpl() (R.Routes, E.Error) { func (p *FileProvider) loadRoutesImpl() (route.Routes, E.Error) {
routes := R.NewRoutes() routes := route.NewRoutes()
entries := entry.NewProxyEntries() entries := route.NewProxyEntries()
data, err := os.ReadFile(p.path) data, err := os.ReadFile(p.path)
if err != nil { if err != nil {
@ -61,7 +60,7 @@ func (p *FileProvider) loadRoutesImpl() (R.Routes, E.Error) {
E.LogWarn("validation failure", err.Subject(p.fileName)) E.LogWarn("validation failure", err.Subject(p.fileName))
} }
return R.FromEntries(entries) return route.FromEntries(entries)
} }
func (p *FileProvider) NewWatcher() W.Watcher { func (p *FileProvider) NewWatcher() W.Watcher {

View file

@ -4,7 +4,8 @@ import (
"github.com/yusing/go-proxy/internal/docker" "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
url "github.com/yusing/go-proxy/internal/net/types" 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" "github.com/yusing/go-proxy/internal/task"
U "github.com/yusing/go-proxy/internal/utils" U "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 +17,7 @@ type (
_ U.NoCopy _ U.NoCopy
impl impl
Type RouteType Type RouteType
Entry *entry.RawEntry Entry *RawEntry
} }
Routes = F.Map[string, *Route] Routes = F.Map[string, *Route]
@ -27,6 +28,8 @@ type (
String() string String() string
TargetURL() url.URL TargetURL() url.URL
} }
RawEntry = types.RawEntry
RawEntries = types.RawEntries
) )
const ( const (
@ -36,6 +39,7 @@ const (
// function alias. // function alias.
var NewRoutes = F.NewMap[Routes] var NewRoutes = F.NewMap[Routes]
var NewProxyEntries = types.NewProxyEntries
func (rt *Route) Container() *docker.Container { func (rt *Route) Container() *docker.Container {
if rt.Entry.Container == nil { if rt.Entry.Container == nil {
@ -44,7 +48,7 @@ func (rt *Route) Container() *docker.Container {
return rt.Entry.Container return rt.Entry.Container
} }
func NewRoute(raw *entry.RawEntry) (*Route, E.Error) { func NewRoute(raw *RawEntry) (*Route, E.Error) {
raw.Finalize() raw.Finalize()
en, err := entry.ValidateEntry(raw) en, err := entry.ValidateEntry(raw)
if err != nil { if err != nil {
@ -74,11 +78,11 @@ func NewRoute(raw *entry.RawEntry) (*Route, E.Error) {
}, nil }, nil
} }
func FromEntries(entries entry.RawEntries) (Routes, E.Error) { func FromEntries(entries RawEntries) (Routes, E.Error) {
b := E.NewBuilder("errors in routes") b := E.NewBuilder("errors in routes")
routes := NewRoutes() routes := NewRoutes()
entries.RangeAllParallel(func(alias string, en *entry.RawEntry) { entries.RangeAllParallel(func(alias string, en *RawEntry) {
en.Alias = alias en.Alias = alias
r, err := NewRoute(en) r, err := NewRoute(en)
switch { switch {

View file

@ -1,4 +1,4 @@
package route package routes
import "github.com/yusing/go-proxy/internal/metrics" import "github.com/yusing/go-proxy/internal/metrics"

View file

@ -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)
}

View file

@ -9,9 +9,9 @@ import (
"github.com/yusing/go-proxy/internal/docker/idlewatcher" "github.com/yusing/go-proxy/internal/docker/idlewatcher"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
net "github.com/yusing/go-proxy/internal/net/types" 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" "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"
"github.com/yusing/go-proxy/internal/watcher/health/monitor" "github.com/yusing/go-proxy/internal/watcher/health/monitor"
) )
@ -19,7 +19,7 @@ import (
type StreamRoute struct { type StreamRoute struct {
*entry.StreamEntry *entry.StreamEntry
stream net.Stream net.Stream
HealthMon health.HealthMonitor `json:"health"` HealthMon health.HealthMonitor `json:"health"`
@ -28,12 +28,6 @@ type StreamRoute struct {
l zerolog.Logger 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) { func NewStreamRoute(entry *entry.StreamEntry) (impl, E.Error) {
// TODO: support non-coherent scheme // TODO: support non-coherent scheme
if !entry.Scheme.IsCoherent() { if !entry.Scheme.IsCoherent() {
@ -60,29 +54,29 @@ func (r *StreamRoute) Start(providerSubtask task.Task) E.Error {
} }
r.task = providerSubtask r.task = providerSubtask
r.stream = NewStream(r) r.Stream = NewStream(r)
switch { switch {
case entry.UseIdleWatcher(r): case entry.UseIdleWatcher(r):
wakerTask := providerSubtask.Parent().Subtask("waker for " + string(r.Alias)) 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 { if err != nil {
r.task.Finish(err) r.task.Finish(err)
return err return err
} }
r.stream = waker r.Stream = waker
r.HealthMon = waker r.HealthMon = waker
case entry.UseHealthCheck(r): case entry.UseHealthCheck(r):
r.HealthMon = monitor.NewRawHealthMonitor(r.TargetURL(), r.HealthCheck) 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) r.task.Finish(err)
return E.From(err) return E.From(err)
} }
r.task.OnFinished("close stream", func() { 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) E.LogError("close stream failed", err, &r.l)
} }
}) })
@ -101,9 +95,9 @@ func (r *StreamRoute) Start(providerSubtask task.Task) E.Error {
go r.acceptConnections() go r.acceptConnections()
streamRoutes.Store(string(r.Alias), r) routes.SetStreamRoute(string(r.Alias), r)
r.task.OnFinished("remove from route table", func() { r.task.OnFinished("remove from route table", func() {
streamRoutes.Delete(string(r.Alias)) routes.DeleteStreamRoute(string(r.Alias))
}) })
return nil return nil
} }
@ -120,7 +114,7 @@ func (r *StreamRoute) acceptConnections() {
case <-r.task.Context().Done(): case <-r.task.Context().Done():
return return
default: default:
conn, err := r.stream.Accept() conn, err := r.Stream.Accept()
if err != nil { if err != nil {
select { select {
case <-r.task.Context().Done(): case <-r.task.Context().Done():
@ -135,7 +129,7 @@ func (r *StreamRoute) acceptConnections() {
} }
connTask := r.task.Subtask("connection") connTask := r.task.Subtask("connection")
go func() { go func() {
err := r.stream.Handle(conn) err := r.Stream.Handle(conn)
if err != nil && !errors.Is(err, context.Canceled) { if err != nil && !errors.Is(err, context.Canceled) {
E.LogError("handle connection error", err, &r.l) E.LogError("handle connection error", err, &r.l)
connTask.Finish(err) connTask.Finish(err)

View file

@ -8,7 +8,7 @@ import (
"time" "time"
"github.com/yusing/go-proxy/internal/net/types" "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" U "github.com/yusing/go-proxy/internal/utils"
) )

View file

@ -1,3 +1,3 @@
package fields package types
type Alias string type Alias string

View file

@ -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
}

View file

@ -1,4 +1,4 @@
package fields package types
import ( import (
"net/http" "net/http"

View file

@ -1,4 +1,4 @@
package fields package types
type ( type (
Host string Host string

View file

@ -1,4 +1,4 @@
package fields package types
import ( import (
"errors" "errors"

View file

@ -1,4 +1,4 @@
package fields package types
import ( import (
"errors" "errors"

View file

@ -1,4 +1,4 @@
package fields package types
import ( import (
"strconv" "strconv"

View file

@ -1,4 +1,4 @@
package entry package types
import ( import (
"strconv" "strconv"
@ -9,7 +9,7 @@ import (
"github.com/yusing/go-proxy/internal/docker" "github.com/yusing/go-proxy/internal/docker"
"github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/logging" "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" U "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"
"github.com/yusing/go-proxy/internal/utils/strutils" "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 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 PathPatterns []string `json:"path_patterns,omitempty" yaml:"path_patterns"` // http(s) proxy only
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty" yaml:"healthcheck"` 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"` Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty" yaml:"middlewares"`
Homepage *homepage.Item `json:"homepage,omitempty" yaml:"homepage"` Homepage *homepage.Item `json:"homepage,omitempty" yaml:"homepage"`

View file

@ -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
}
)

View file

@ -1,4 +1,4 @@
package fields package types
import ( import (
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"

View file

@ -1,4 +1,4 @@
package fields package types
import ( import (
"strings" "strings"

View file

@ -1,4 +1,4 @@
package fields package types
import ( import (
"strconv" "strconv"

View file

@ -1,4 +1,4 @@
package fields package types
import ( import (
"fmt" "fmt"

View file

@ -1,4 +1,4 @@
package fields package types
import ( import (
"testing" "testing"