mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 17:28:31 +02:00
Feat/fileserver (#60)
* cleanup code for URL type * fix makefile for trace mode * refactor, merge Entry, RawEntry and Route into one. * Implement fileserver. * refactor: rename HTTPRoute to ReverseProxyRoute to avoid confusion * refactor: move metrics logger to middleware package - fix prometheus metrics for load balanced routes - route will now fail when health monitor fail to start * fix extra output of ls-* commands by defer initializaing stuff, speed up start time * add test for path traversal attack, small fix on FileServer.Start method * rename rule.on.bypass to pass * refactor and fixed map-to-map deserialization * updated route loading logic * schemas: add "add_prefix" option to modify_request middleware * updated route JSONMarshalling --------- Co-authored-by: yusing <yusing@6uo.me>
This commit is contained in:
413
internal/route/route.go
Executable file → Normal file
413
internal/route/route.go
Executable file → Normal file
@@ -1,104 +1,367 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/route/entry"
|
||||
"github.com/yusing/go-proxy/internal/route/types"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
net "github.com/yusing/go-proxy/internal/net/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"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
|
||||
dockertypes "github.com/docker/docker/api/types"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
||||
"github.com/yusing/go-proxy/internal/route/rules"
|
||||
"github.com/yusing/go-proxy/internal/route/types"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
Route struct {
|
||||
_ U.NoCopy
|
||||
impl
|
||||
Type types.RouteType
|
||||
Entry *RawEntry
|
||||
}
|
||||
Routes = F.Map[string, *Route]
|
||||
_ utils.NoCopy
|
||||
|
||||
impl interface {
|
||||
types.Route
|
||||
task.TaskStarter
|
||||
task.TaskFinisher
|
||||
String() string
|
||||
TargetURL() url.URL
|
||||
Alias string `json:"alias"`
|
||||
Scheme types.Scheme `json:"scheme,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Port types.Port `json:"port,omitempty"`
|
||||
Root string `json:"root,omitempty"`
|
||||
|
||||
types.HTTPConfig
|
||||
PathPatterns []string `json:"path_patterns,omitempty"`
|
||||
Rules rules.Rules `json:"rules,omitempty" validate:"omitempty,unique=Name"`
|
||||
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
|
||||
LoadBalance *loadbalance.Config `json:"load_balance,omitempty"`
|
||||
Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty"`
|
||||
Homepage *homepage.Item `json:"homepage,omitempty"`
|
||||
AccessLog *accesslog.Config `json:"access_log,omitempty"`
|
||||
|
||||
Metadata `deserialize:"-"`
|
||||
}
|
||||
RawEntry = types.RawEntry
|
||||
RawEntries = types.RawEntries
|
||||
|
||||
Metadata struct {
|
||||
/* Docker only */
|
||||
Container *docker.Container `json:"container,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
|
||||
// private fields
|
||||
LisURL *net.URL `json:"lurl,omitempty"`
|
||||
ProxyURL *net.URL `json:"purl,omitempty"`
|
||||
Idlewatcher *idlewatcher.Config `json:"idlewatcher,omitempty"`
|
||||
|
||||
impl types.Route
|
||||
isValidated bool
|
||||
}
|
||||
Routes map[string]*Route
|
||||
)
|
||||
|
||||
// function alias.
|
||||
var (
|
||||
NewRoutes = F.NewMap[Routes]
|
||||
NewProxyEntries = types.NewProxyEntries
|
||||
)
|
||||
|
||||
func (rt *Route) Container() *docker.Container {
|
||||
if rt.Entry.Container == nil {
|
||||
return docker.DummyContainer
|
||||
}
|
||||
return rt.Entry.Container
|
||||
func (r Routes) Contains(alias string) bool {
|
||||
_, ok := r[alias]
|
||||
return ok
|
||||
}
|
||||
|
||||
func NewRoute(raw *RawEntry) (*Route, E.Error) {
|
||||
raw.Finalize()
|
||||
en, err := entry.ValidateEntry(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (r *Route) Validate() (err E.Error) {
|
||||
if r.isValidated {
|
||||
return nil
|
||||
}
|
||||
r.isValidated = true
|
||||
r.Finalize()
|
||||
|
||||
var t types.RouteType
|
||||
var rt impl
|
||||
errs := E.NewBuilder("entry validation failed")
|
||||
|
||||
switch e := en.(type) {
|
||||
case *entry.StreamEntry:
|
||||
t = types.RouteTypeStream
|
||||
rt, err = NewStreamRoute(e)
|
||||
case *entry.ReverseProxyEntry:
|
||||
t = types.RouteTypeReverseProxy
|
||||
rt, err = NewHTTPRoute(e)
|
||||
switch r.Scheme {
|
||||
case types.SchemeFileServer:
|
||||
r.impl, err = NewFileServer(r)
|
||||
case types.SchemeHTTP, types.SchemeHTTPS:
|
||||
if r.Port.Listening != 0 {
|
||||
errs.Addf("unexpected listening port for %s scheme", r.Scheme)
|
||||
}
|
||||
fallthrough
|
||||
case types.SchemeTCP, types.SchemeUDP:
|
||||
r.LisURL = E.Collect(errs, net.ParseURL, fmt.Sprintf("%s://%s:%d", r.Scheme, r.Host, r.Port.Listening))
|
||||
fallthrough
|
||||
default:
|
||||
panic("bug: should not reach here")
|
||||
if r.Port.Proxy == 0 && !r.IsDocker() {
|
||||
errs.Adds("missing proxy port")
|
||||
}
|
||||
if r.LoadBalance != nil && r.LoadBalance.Link == "" {
|
||||
r.LoadBalance = nil
|
||||
}
|
||||
r.ProxyURL = E.Collect(errs, net.ParseURL, fmt.Sprintf("%s://%s:%d", r.Scheme, r.Host, r.Port.Proxy))
|
||||
r.Idlewatcher = E.Collect(errs, idlewatcher.ValidateConfig, r.Container)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
if !r.UseHealthCheck() && (r.UseLoadBalance() || r.UseIdleWatcher()) {
|
||||
errs.Adds("healthCheck.disable cannot be true when loadbalancer or idlewatcher is enabled")
|
||||
}
|
||||
return &Route{
|
||||
impl: rt,
|
||||
Type: t,
|
||||
Entry: raw,
|
||||
}, nil
|
||||
|
||||
if errs.HasError() {
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
switch r.Scheme {
|
||||
case types.SchemeFileServer:
|
||||
r.impl, err = NewFileServer(r)
|
||||
case types.SchemeHTTP, types.SchemeHTTPS:
|
||||
r.impl, err = NewReverseProxyRoute(r)
|
||||
case types.SchemeTCP, types.SchemeUDP:
|
||||
r.impl, err = NewStreamRoute(r)
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected scheme %s for alias %s", r.Scheme, r.Alias))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func FromEntries(provider string, entries RawEntries) (Routes, E.Error) {
|
||||
b := E.NewBuilder("errors in routes")
|
||||
func (r *Route) Start(parent task.Parent) (err E.Error) {
|
||||
if r.impl == nil {
|
||||
return E.New("route not initialized")
|
||||
}
|
||||
return r.impl.Start(parent)
|
||||
}
|
||||
|
||||
routes := NewRoutes()
|
||||
entries.RangeAllParallel(func(alias string, en *RawEntry) {
|
||||
if en == nil {
|
||||
en = new(RawEntry)
|
||||
}
|
||||
en.Alias = alias
|
||||
en.Provider = provider
|
||||
if strings.HasPrefix(alias, "x-") { // x properties
|
||||
return
|
||||
}
|
||||
r, err := NewRoute(en)
|
||||
func (r *Route) Finish(reason any) {
|
||||
if r.impl == nil {
|
||||
return
|
||||
}
|
||||
r.impl.Finish(reason)
|
||||
r.impl = nil
|
||||
}
|
||||
|
||||
func (r *Route) Started() bool {
|
||||
return r.impl != nil
|
||||
}
|
||||
|
||||
func (r *Route) ProviderName() string {
|
||||
return r.Provider
|
||||
}
|
||||
|
||||
func (r *Route) TargetName() string {
|
||||
return r.Alias
|
||||
}
|
||||
|
||||
func (r *Route) TargetURL() *net.URL {
|
||||
return r.ProxyURL
|
||||
}
|
||||
|
||||
func (r *Route) Type() types.RouteType {
|
||||
switch r.Scheme {
|
||||
case types.SchemeHTTP, types.SchemeHTTPS, types.SchemeFileServer:
|
||||
return types.RouteTypeHTTP
|
||||
case types.SchemeTCP, types.SchemeUDP:
|
||||
return types.RouteTypeStream
|
||||
}
|
||||
panic(fmt.Errorf("unexpected scheme %s for alias %s", r.Scheme, r.Alias))
|
||||
}
|
||||
|
||||
func (r *Route) HealthMonitor() health.HealthMonitor {
|
||||
return r.impl.HealthMonitor()
|
||||
}
|
||||
|
||||
func (r *Route) IdlewatcherConfig() *idlewatcher.Config {
|
||||
return r.Idlewatcher
|
||||
}
|
||||
|
||||
func (r *Route) HealthCheckConfig() *health.HealthCheckConfig {
|
||||
return r.HealthCheck
|
||||
}
|
||||
|
||||
func (r *Route) LoadBalanceConfig() *loadbalance.Config {
|
||||
return r.LoadBalance
|
||||
}
|
||||
|
||||
func (r *Route) HomepageConfig() *homepage.Item {
|
||||
return r.Homepage
|
||||
}
|
||||
|
||||
func (r *Route) ContainerInfo() *docker.Container {
|
||||
return r.Container
|
||||
}
|
||||
|
||||
func (r *Route) IsDocker() bool {
|
||||
if r.Container == nil {
|
||||
return false
|
||||
}
|
||||
return r.Container.ContainerID != ""
|
||||
}
|
||||
|
||||
func (r *Route) IsZeroPort() bool {
|
||||
return r.Port.Proxy == 0
|
||||
}
|
||||
|
||||
func (r *Route) ShouldExclude() bool {
|
||||
if r.Container != nil {
|
||||
switch {
|
||||
case err != nil:
|
||||
b.Add(err.Subject(alias))
|
||||
case entry.ShouldNotServe(r):
|
||||
return
|
||||
default:
|
||||
routes.Store(alias, r)
|
||||
case r.Container.IsExcluded:
|
||||
logging.Debug().Str("container", r.Container.ContainerName).Msg("container excluded: explicitly excluded")
|
||||
return true
|
||||
case r.IsZeroPort() && !r.UseIdleWatcher():
|
||||
logging.Debug().Str("container", r.Container.ContainerName).Msg("container excluded: zero port and no idle watcher")
|
||||
return true
|
||||
case r.Container.IsDatabase && !r.Container.IsExplicit:
|
||||
logging.Debug().Str("container", r.Container.ContainerName).Msg("container excluded: database")
|
||||
return true
|
||||
case strings.HasPrefix(r.Container.ContainerName, "buildx_"):
|
||||
logging.Debug().Str("container", r.Container.ContainerName).Msg("container excluded: buildx prefix")
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
return routes, b.Error()
|
||||
} else if r.IsZeroPort() {
|
||||
logging.Debug().Str("container", r.Container.ContainerName).Msg("container excluded: zero port")
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(r.Alias, "x-") ||
|
||||
strings.HasSuffix(r.Alias, "-old") {
|
||||
logging.Debug().Str("container", r.Container.ContainerName).Msg("container excluded: alias")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *Route) UseLoadBalance() bool {
|
||||
return r.LoadBalance != nil && r.LoadBalance.Link != ""
|
||||
}
|
||||
|
||||
func (r *Route) UseIdleWatcher() bool {
|
||||
return r.Idlewatcher != nil && r.Idlewatcher.IdleTimeout > 0
|
||||
}
|
||||
|
||||
func (r *Route) UseHealthCheck() bool {
|
||||
return !r.HealthCheck.Disable
|
||||
}
|
||||
|
||||
func (r *Route) UseAccessLog() bool {
|
||||
return r.AccessLog != nil
|
||||
}
|
||||
|
||||
func (r *Route) Finalize() {
|
||||
isDocker := r.Container != nil
|
||||
cont := r.Container
|
||||
|
||||
if r.Host == "" {
|
||||
switch {
|
||||
case !isDocker:
|
||||
r.Host = "localhost"
|
||||
case cont.PrivateIP != "":
|
||||
r.Host = cont.PrivateIP
|
||||
case cont.PublicIP != "":
|
||||
r.Host = cont.PublicIP
|
||||
}
|
||||
}
|
||||
|
||||
lp, pp := r.Port.Listening, r.Port.Proxy
|
||||
|
||||
if isDocker {
|
||||
if port, ok := common.ServiceNamePortMapTCP[cont.ImageName]; ok {
|
||||
if pp == 0 {
|
||||
pp = port
|
||||
}
|
||||
if r.Scheme == "" {
|
||||
r.Scheme = "tcp"
|
||||
}
|
||||
} else if port, ok := common.ImageNamePortMap[cont.ImageName]; ok {
|
||||
if pp == 0 {
|
||||
pp = port
|
||||
}
|
||||
if r.Scheme == "" {
|
||||
r.Scheme = "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pp == 0 {
|
||||
switch {
|
||||
case r.Scheme == "https":
|
||||
pp = 443
|
||||
case !isDocker:
|
||||
pp = 80
|
||||
default:
|
||||
pp = lowestPort(cont.PrivatePortMapping)
|
||||
if pp == 0 {
|
||||
pp = lowestPort(cont.PublicPortMapping)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isDocker {
|
||||
// replace private port with public port if using public IP.
|
||||
if r.Host == cont.PublicIP {
|
||||
if p, ok := cont.PrivatePortMapping[pp]; ok {
|
||||
pp = int(p.PublicPort)
|
||||
}
|
||||
}
|
||||
// replace public port with private port if using private IP.
|
||||
if r.Host == cont.PrivateIP {
|
||||
if p, ok := cont.PublicPortMapping[pp]; ok {
|
||||
pp = int(p.PrivatePort)
|
||||
}
|
||||
}
|
||||
|
||||
if r.Scheme == "" {
|
||||
switch {
|
||||
case r.Host == cont.PublicIP && cont.PublicPortMapping[pp].Type == "udp":
|
||||
r.Scheme = "udp"
|
||||
case r.Host == cont.PrivateIP && cont.PrivatePortMapping[pp].Type == "udp":
|
||||
r.Scheme = "udp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.Scheme == "" {
|
||||
switch {
|
||||
case lp != 0:
|
||||
r.Scheme = "tcp"
|
||||
case strings.HasSuffix(strconv.Itoa(pp), "443"):
|
||||
r.Scheme = "https"
|
||||
default: // assume its http
|
||||
r.Scheme = "http"
|
||||
}
|
||||
}
|
||||
|
||||
r.Port.Listening, r.Port.Proxy = lp, pp
|
||||
|
||||
if r.HealthCheck == nil {
|
||||
r.HealthCheck = health.DefaultHealthConfig
|
||||
}
|
||||
|
||||
if !r.HealthCheck.Disable {
|
||||
if r.HealthCheck.Interval == 0 {
|
||||
r.HealthCheck.Interval = common.HealthCheckIntervalDefault
|
||||
}
|
||||
if r.HealthCheck.Timeout == 0 {
|
||||
r.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
|
||||
}
|
||||
}
|
||||
|
||||
if isDocker && cont.IdleTimeout != "" {
|
||||
if cont.WakeTimeout == "" {
|
||||
cont.WakeTimeout = common.WakeTimeoutDefault
|
||||
}
|
||||
if cont.StopTimeout == "" {
|
||||
cont.StopTimeout = common.StopTimeoutDefault
|
||||
}
|
||||
if cont.StopMethod == "" {
|
||||
cont.StopMethod = common.StopMethodDefault
|
||||
}
|
||||
}
|
||||
|
||||
if r.Homepage.IsEmpty() {
|
||||
r.Homepage = homepage.NewItem(r.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
func lowestPort(ports map[int]dockertypes.Port) (res int) {
|
||||
cmp := (uint16)(65535)
|
||||
for port, v := range ports {
|
||||
if v.PrivatePort < cmp {
|
||||
cmp = v.PrivatePort
|
||||
res = port
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user