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

@@ -0,0 +1,58 @@
package entry
import (
E "github.com/yusing/go-proxy/internal/error"
route "github.com/yusing/go-proxy/internal/route/types"
)
type Entry = route.Entry
func ValidateEntry(m *route.RawEntry) (Entry, E.Error) {
scheme, err := route.NewScheme(m.Scheme)
if err != nil {
return nil, E.From(err)
}
var entry Entry
errs := E.NewBuilder("entry validation failed")
if scheme.IsStream() {
entry = validateStreamEntry(m, errs)
} else {
entry = validateRPEntry(m, scheme, errs)
}
if errs.HasError() {
return nil, errs.Error()
}
if !UseHealthCheck(entry) && (UseLoadBalance(entry) || UseIdleWatcher(entry)) {
return nil, E.New("healthCheck.disable cannot be true when loadbalancer or idlewatcher is enabled")
}
return entry, nil
}
func IsDocker(entry Entry) bool {
iw := entry.IdlewatcherConfig()
return iw != nil && iw.ContainerID != ""
}
func IsZeroPort(entry Entry) bool {
return entry.TargetURL().Port() == "0"
}
func ShouldNotServe(entry Entry) bool {
return IsZeroPort(entry) && !UseIdleWatcher(entry)
}
func UseLoadBalance(entry Entry) bool {
lb := entry.LoadBalanceConfig()
return lb != nil && lb.Link != ""
}
func UseIdleWatcher(entry Entry) bool {
iw := entry.IdlewatcherConfig()
return iw != nil && iw.IdleTimeout > 0
}
func UseHealthCheck(entry Entry) bool {
hc := entry.HealthCheckConfig()
return hc != nil && !hc.Disable
}

View File

@@ -0,0 +1,89 @@
package entry
import (
"fmt"
"net/url"
"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"
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
net "github.com/yusing/go-proxy/internal/net/types"
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 *route.RawEntry `json:"raw"`
Alias route.Alias `json:"alias"`
Scheme route.Scheme `json:"scheme"`
URL net.URL `json:"url"`
NoTLSVerify bool `json:"no_tls_verify,omitempty"`
PathPatterns route.PathPatterns `json:"path_patterns,omitempty"`
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
LoadBalance *loadbalance.Config `json:"load_balance,omitempty"`
Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty"`
/* Docker only */
Idlewatcher *idlewatcher.Config `json:"idlewatcher,omitempty"`
}
func (rp *ReverseProxyEntry) TargetName() string {
return string(rp.Alias)
}
func (rp *ReverseProxyEntry) TargetURL() net.URL {
return rp.URL
}
func (rp *ReverseProxyEntry) RawEntry() *route.RawEntry {
return rp.Raw
}
func (rp *ReverseProxyEntry) LoadBalanceConfig() *loadbalance.Config {
return rp.LoadBalance
}
func (rp *ReverseProxyEntry) HealthCheckConfig() *health.HealthCheckConfig {
return rp.HealthCheck
}
func (rp *ReverseProxyEntry) IdlewatcherConfig() *idlewatcher.Config {
return rp.Idlewatcher
}
func validateRPEntry(m *route.RawEntry, s route.Scheme, errs *E.Builder) *ReverseProxyEntry {
cont := m.Container
if cont == nil {
cont = docker.DummyContainer
}
lb := m.LoadBalance
if lb != nil && lb.Link == "" {
lb = nil
}
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)
if errs.HasError() {
return nil
}
return &ReverseProxyEntry{
Raw: m,
Alias: route.Alias(m.Alias),
Scheme: s,
URL: net.NewURL(url),
NoTLSVerify: m.NoTLSVerify,
PathPatterns: pathPats,
HealthCheck: m.HealthCheck,
LoadBalance: lb,
Middlewares: m.Middlewares,
Idlewatcher: iwCfg,
}
}

View File

@@ -0,0 +1,80 @@
package entry
import (
"fmt"
"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"
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
net "github.com/yusing/go-proxy/internal/net/types"
route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/watcher/health"
)
type StreamEntry struct {
Raw *route.RawEntry `json:"raw"`
Alias route.Alias `json:"alias"`
Scheme route.StreamScheme `json:"scheme"`
URL net.URL `json:"url"`
Host route.Host `json:"host,omitempty"`
Port route.StreamPort `json:"port,omitempty"`
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
/* Docker only */
Idlewatcher *idlewatcher.Config `json:"idlewatcher,omitempty"`
}
func (s *StreamEntry) TargetName() string {
return string(s.Alias)
}
func (s *StreamEntry) TargetURL() net.URL {
return s.URL
}
func (s *StreamEntry) RawEntry() *route.RawEntry {
return s.Raw
}
func (s *StreamEntry) LoadBalanceConfig() *loadbalance.Config {
// TODO: support stream load balance
return nil
}
func (s *StreamEntry) HealthCheckConfig() *health.HealthCheckConfig {
return s.HealthCheck
}
func (s *StreamEntry) IdlewatcherConfig() *idlewatcher.Config {
return s.Idlewatcher
}
func validateStreamEntry(m *route.RawEntry, errs *E.Builder) *StreamEntry {
cont := m.Container
if cont == nil {
cont = docker.DummyContainer
}
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)
if errs.HasError() {
return nil
}
return &StreamEntry{
Raw: m,
Alias: route.Alias(m.Alias),
Scheme: *scheme,
URL: url,
Host: host,
Port: port,
HealthCheck: m.HealthCheck,
Idlewatcher: idleWatcherCfg,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package route
package routes
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"
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)

View File

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

View File

@@ -0,0 +1,3 @@
package types
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

@@ -0,0 +1,19 @@
package types
import (
"net/http"
"strings"
E "github.com/yusing/go-proxy/internal/error"
)
func ValidateHTTPHeaders(headers map[string]string) (http.Header, E.Error) {
h := make(http.Header)
for k, v := range headers {
vSplit := strings.Split(v, ",")
for _, header := range vSplit {
h.Add(k, strings.TrimSpace(header))
}
}
return h, nil
}

View File

@@ -0,0 +1,10 @@
package types
type (
Host string
Subdomain = Alias
)
func ValidateHost[String ~string](s String) (Host, error) {
return Host(s), nil
}

View File

@@ -0,0 +1,48 @@
package types
import (
"errors"
"fmt"
"regexp"
E "github.com/yusing/go-proxy/internal/error"
)
type (
PathPattern string
PathPatterns = []PathPattern
)
var pathPattern = regexp.MustCompile(`^(/[-\w./]*({\$\})?|((GET|POST|DELETE|PUT|HEAD|OPTION) /[-\w./]*({\$\})?))$`)
var (
ErrEmptyPathPattern = errors.New("path must not be empty")
ErrInvalidPathPattern = errors.New("invalid path pattern")
)
func ValidatePathPattern(s string) (PathPattern, error) {
if len(s) == 0 {
return "", ErrEmptyPathPattern
}
if !pathPattern.MatchString(s) {
return "", fmt.Errorf("%w %q", ErrInvalidPathPattern, s)
}
return PathPattern(s), nil
}
func ValidatePathPatterns(s []string) (PathPatterns, E.Error) {
if len(s) == 0 {
return nil, nil
}
errs := E.NewBuilder("invalid path patterns")
pp := make(PathPatterns, len(s))
for i, v := range s {
pattern, err := ValidatePathPattern(v)
if err != nil {
errs.Add(err)
} else {
pp[i] = pattern
}
}
return pp, errs.Error()
}

View File

@@ -0,0 +1,47 @@
package types
import (
"errors"
"testing"
U "github.com/yusing/go-proxy/internal/utils/testing"
)
var validPatterns = []string{
"/",
"/index.html",
"/somepage/",
"/drive/abc.mp4",
"/{$}",
"/some-page/{$}",
"GET /",
"GET /static/{$}",
"GET /drive/abc.mp4",
"GET /drive/abc.mp4/{$}",
"POST /auth",
"DELETE /user/",
"PUT /storage/id/",
}
var invalidPatterns = []string{
"/$",
"/{$}{$}",
"/{$}/{$}",
"/index.html$",
"get /",
"GET/",
"GET /$",
"GET /drive/{$}/abc.mp4/",
"OPTION /config/{$}/abc.conf/{$}",
}
func TestPathPatternRegex(t *testing.T) {
for _, pattern := range validPatterns {
_, err := ValidatePathPattern(pattern)
U.ExpectNoError(t, err)
}
for _, pattern := range invalidPatterns {
_, err := ValidatePathPattern(pattern)
U.ExpectTrue(t, errors.Is(err, ErrInvalidPathPattern))
}
}

View File

@@ -0,0 +1,43 @@
package types
import (
"strconv"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
type Port int
var ErrPortOutOfRange = E.New("port out of range")
func ValidatePort[String ~string](v String) (Port, error) {
p, err := strutils.Atoi(string(v))
if err != nil {
return ErrPort, err
}
return ValidatePortInt(p)
}
func ValidatePortInt[Int int | uint16](v Int) (Port, error) {
p := Port(v)
if !p.inBound() {
return ErrPort, ErrPortOutOfRange.Subject(strconv.Itoa(int(p)))
}
return p, nil
}
func (p Port) inBound() bool {
return p >= MinPort && p <= MaxPort
}
func (p Port) String() string {
return strconv.Itoa(int(p))
}
const (
MinPort = 0
MaxPort = 65535
ErrPort = Port(-1)
NoPort = Port(0)
)

View File

@@ -0,0 +1,203 @@
package types
import (
"strconv"
"strings"
"github.com/docker/docker/api/types"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/docker"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/logging"
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"
"github.com/yusing/go-proxy/internal/watcher/health"
)
type (
RawEntry struct {
_ U.NoCopy
// raw entry object before validation
// loaded from docker labels or yaml file
Alias string `json:"-" yaml:"-"`
Scheme string `json:"scheme,omitempty" yaml:"scheme"`
Host string `json:"host,omitempty" yaml:"host"`
Port string `json:"port,omitempty" yaml:"port"`
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 *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"`
/* Docker only */
Container *docker.Container `json:"container,omitempty" yaml:"-"`
finalized bool
}
RawEntries = F.Map[string, *RawEntry]
)
var NewProxyEntries = F.NewMapOf[string, *RawEntry]
func (e *RawEntry) Finalize() {
if e.finalized {
return
}
isDocker := e.Container != nil
cont := e.Container
if !isDocker {
cont = docker.DummyContainer
}
if e.Host == "" {
switch {
case cont.PrivateIP != "":
e.Host = cont.PrivateIP
case cont.PublicIP != "":
e.Host = cont.PublicIP
case !isDocker:
e.Host = "localhost"
}
}
lp, pp, extra := e.splitPorts()
if port, ok := common.ServiceNamePortMapTCP[cont.ImageName]; ok {
if pp == "" {
pp = strconv.Itoa(port)
}
if e.Scheme == "" {
e.Scheme = "tcp"
}
} else if port, ok := common.ImageNamePortMap[cont.ImageName]; ok {
if pp == "" {
pp = strconv.Itoa(port)
}
if e.Scheme == "" {
e.Scheme = "http"
}
} else if pp == "" && e.Scheme == "https" {
pp = "443"
} else if pp == "" {
if p := lowestPort(cont.PrivatePortMapping); p != "" {
pp = p
} else if p := lowestPort(cont.PublicPortMapping); p != "" {
pp = p
} else if !isDocker {
pp = "80"
} else {
logging.Debug().Msg("no port found for " + e.Alias)
}
}
// replace private port with public port if using public IP.
if e.Host == cont.PublicIP {
if p, ok := cont.PrivatePortMapping[pp]; ok {
pp = strutils.PortString(p.PublicPort)
}
}
// replace public port with private port if using private IP.
if e.Host == cont.PrivateIP {
if p, ok := cont.PublicPortMapping[pp]; ok {
pp = strutils.PortString(p.PrivatePort)
}
}
if e.Scheme == "" && isDocker {
switch {
case e.Host == cont.PublicIP && cont.PublicPortMapping[pp].Type == "udp":
e.Scheme = "udp"
case e.Host == cont.PrivateIP && cont.PrivatePortMapping[pp].Type == "udp":
e.Scheme = "udp"
}
}
if e.Scheme == "" {
switch {
case lp != "":
e.Scheme = "tcp"
case strings.HasSuffix(pp, "443"):
e.Scheme = "https"
default: // assume its http
e.Scheme = "http"
}
}
if e.HealthCheck == nil {
e.HealthCheck = health.DefaultHealthCheckConfig()
}
if e.HealthCheck.Disable {
e.HealthCheck = nil
}
if cont.IdleTimeout != "" {
if cont.WakeTimeout == "" {
cont.WakeTimeout = common.WakeTimeoutDefault
}
if cont.StopTimeout == "" {
cont.StopTimeout = common.StopTimeoutDefault
}
if cont.StopMethod == "" {
cont.StopMethod = common.StopMethodDefault
}
}
e.Port = joinPorts(lp, pp, extra)
if e.Port == "" || e.Host == "" {
if lp != "" {
e.Port = lp + ":0"
} else {
e.Port = "0"
}
}
e.finalized = true
}
func (e *RawEntry) splitPorts() (lp string, pp string, extra string) {
portSplit := strings.Split(e.Port, ":")
if len(portSplit) == 1 {
pp = portSplit[0]
} else {
lp = portSplit[0]
pp = portSplit[1]
if len(portSplit) > 2 {
extra = strings.Join(portSplit[2:], ":")
}
}
return
}
func joinPorts(lp string, pp string, extra string) string {
s := make([]string, 0, 3)
if lp != "" {
s = append(s, lp)
}
if pp != "" {
s = append(s, pp)
}
if extra != "" {
s = append(s, extra)
}
return strings.Join(s, ":")
}
func lowestPort(ports map[string]types.Port) string {
var cmp uint16
var res string
for port, v := range ports {
if v.PrivatePort < cmp || cmp == 0 {
cmp = v.PrivatePort
res = port
}
}
return res
}

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

@@ -0,0 +1,23 @@
package types
import (
E "github.com/yusing/go-proxy/internal/error"
)
type Scheme string
var ErrInvalidScheme = E.New("invalid scheme")
func NewScheme(s string) (Scheme, error) {
switch s {
case "http", "https", "tcp", "udp":
return Scheme(s), nil
}
return "", ErrInvalidScheme.Subject(s)
}
func (s Scheme) IsHTTP() bool { return s == "http" }
func (s Scheme) IsHTTPS() bool { return s == "https" }
func (s Scheme) IsTCP() bool { return s == "tcp" }
func (s Scheme) IsUDP() bool { return s == "udp" }
func (s Scheme) IsStream() bool { return s.IsTCP() || s.IsUDP() }

View File

@@ -0,0 +1,35 @@
package types
import (
"strings"
E "github.com/yusing/go-proxy/internal/error"
)
type StreamPort struct {
ListeningPort Port `json:"listening"`
ProxyPort Port `json:"proxy"`
}
var ErrStreamPortTooManyColons = E.New("too many colons")
func ValidateStreamPort(p string) (StreamPort, error) {
split := strings.Split(p, ":")
switch len(split) {
case 1:
split = []string{"0", split[0]}
case 2:
break
default:
return StreamPort{}, ErrStreamPortTooManyColons.Subject(p)
}
listeningPort, lErr := ValidatePort(split[0])
proxyPort, pErr := ValidatePort(split[1])
if err := E.Join(lErr, pErr); err != nil {
return StreamPort{}, err
}
return StreamPort{listeningPort, proxyPort}, nil
}

View File

@@ -0,0 +1,54 @@
package types
import (
"strconv"
"testing"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
var validPorts = []string{
"1234:5678",
"0:2345",
"2345",
}
var invalidPorts = []string{
"",
"123:",
"0:",
":1234",
"qwerty",
"asdfgh:asdfgh",
"1234:asdfgh",
}
var outOfRangePorts = []string{
"-1:1234",
"1234:-1",
"65536",
"0:65536",
}
var tooManyColonsPorts = []string{
"1234:1234:1234",
}
func TestStreamPort(t *testing.T) {
for _, port := range validPorts {
_, err := ValidateStreamPort(port)
ExpectNoError(t, err)
}
for _, port := range invalidPorts {
_, err := ValidateStreamPort(port)
ExpectError2(t, port, strconv.ErrSyntax, err)
}
for _, port := range outOfRangePorts {
_, err := ValidateStreamPort(port)
ExpectError2(t, port, ErrPortOutOfRange, err)
}
for _, port := range tooManyColonsPorts {
_, err := ValidateStreamPort(port)
ExpectError2(t, port, ErrStreamPortTooManyColons, err)
}
}

View File

@@ -0,0 +1,44 @@
package types
import (
"fmt"
"strings"
E "github.com/yusing/go-proxy/internal/error"
)
type StreamScheme struct {
ListeningScheme Scheme `json:"listening"`
ProxyScheme Scheme `json:"proxy"`
}
func ValidateStreamScheme(s string) (*StreamScheme, error) {
ss := &StreamScheme{}
parts := strings.Split(s, ":")
if len(parts) == 1 {
parts = []string{s, s}
} else if len(parts) != 2 {
return nil, ErrInvalidScheme.Subject(s)
}
var lErr, pErr error
ss.ListeningScheme, lErr = NewScheme(parts[0])
ss.ProxyScheme, pErr = NewScheme(parts[1])
if err := E.Join(lErr, pErr); err != nil {
return nil, err
}
return ss, nil
}
func (s StreamScheme) String() string {
return fmt.Sprintf("%s -> %s", s.ListeningScheme, s.ProxyScheme)
}
// IsCoherent checks if the ListeningScheme and ProxyScheme of the StreamScheme are equal.
//
// It returns a boolean value indicating whether the ListeningScheme and ProxyScheme are equal.
func (s StreamScheme) IsCoherent() bool {
return s.ListeningScheme == s.ProxyScheme
}

View File

@@ -0,0 +1,37 @@
package types
import (
"testing"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
var (
validStreamSchemes = []string{
"tcp:tcp",
"tcp:udp",
"udp:tcp",
"udp:udp",
"tcp",
"udp",
}
invalidStreamSchemes = []string{
"tcp:tcp:",
"tcp:",
":udp:",
":udp",
"top",
}
)
func TestNewStreamScheme(t *testing.T) {
for _, s := range validStreamSchemes {
_, err := ValidateStreamScheme(s)
ExpectNoError(t, err)
}
for _, s := range invalidStreamSchemes {
_, err := ValidateStreamScheme(s)
ExpectError(t, ErrInvalidScheme, err)
}
}