mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 09:48:49 +02:00
modules reorganized and code refactor
This commit is contained in:
3
internal/route/types/alias.go
Normal file
3
internal/route/types/alias.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package types
|
||||
|
||||
type Alias string
|
||||
17
internal/route/types/entry.go
Normal file
17
internal/route/types/entry.go
Normal 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
|
||||
}
|
||||
19
internal/route/types/headers.go
Normal file
19
internal/route/types/headers.go
Normal 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
|
||||
}
|
||||
10
internal/route/types/host.go
Normal file
10
internal/route/types/host.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package types
|
||||
|
||||
type (
|
||||
Host string
|
||||
Subdomain = Alias
|
||||
)
|
||||
|
||||
func ValidateHost[String ~string](s String) (Host, error) {
|
||||
return Host(s), nil
|
||||
}
|
||||
48
internal/route/types/path_pattern.go
Normal file
48
internal/route/types/path_pattern.go
Normal 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()
|
||||
}
|
||||
47
internal/route/types/path_pattern_test.go
Normal file
47
internal/route/types/path_pattern_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
43
internal/route/types/port.go
Normal file
43
internal/route/types/port.go
Normal 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)
|
||||
)
|
||||
203
internal/route/types/raw_entry.go
Normal file
203
internal/route/types/raw_entry.go
Normal 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
|
||||
}
|
||||
18
internal/route/types/route.go
Normal file
18
internal/route/types/route.go
Normal 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
|
||||
}
|
||||
)
|
||||
23
internal/route/types/scheme.go
Normal file
23
internal/route/types/scheme.go
Normal 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() }
|
||||
35
internal/route/types/stream_port.go
Normal file
35
internal/route/types/stream_port.go
Normal 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
|
||||
}
|
||||
54
internal/route/types/stream_port_test.go
Normal file
54
internal/route/types/stream_port_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
44
internal/route/types/stream_scheme.go
Normal file
44
internal/route/types/stream_scheme.go
Normal 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
|
||||
}
|
||||
37
internal/route/types/stream_scheme_test.go
Normal file
37
internal/route/types/stream_scheme_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user