Fixed nil dereferencing and added missing fields validation

This commit is contained in:
yusing 2024-09-23 16:14:34 +08:00
parent 6728bc39d2
commit 8e2cc56afb
17 changed files with 316 additions and 117 deletions

View file

@ -0,0 +1,39 @@
From 6728bc39d2d7ded19c1ca288691ec787c02ff666 Mon Sep 17 00:00:00 2001
From: yusing <yusing@6uo.me>
Date: Mon, 23 Sep 2024 07:19:47 +0800
Subject: [PATCH] fixed nil dereferencing
---
frontend | 2 +-
src/error/builder.go | 6 ++++--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/frontend b/frontend
index d0e5963..441fd70 160000
--- a/frontend
+++ b/frontend
@@ -1 +1 @@
-Subproject commit d0e59630d6e0beb1c22d2f242f556464a5056c1f
+Subproject commit 441fd708dbed89c59bf72a742431a78ff2f02bc3
diff --git a/src/error/builder.go b/src/error/builder.go
index 38a1bf2..16d2816 100644
--- a/src/error/builder.go
+++ b/src/error/builder.go
@@ -60,10 +60,12 @@ func (b Builder) Build() NestedError {
}
func (b Builder) To(ptr *NestedError) {
- if *ptr == nil {
+ if ptr == nil {
+ return
+ } else if *ptr == nil {
*ptr = b.Build()
} else {
- **ptr = *b.Build()
+ (*ptr).With(b.Build())
}
}
--
2.46.1

View file

@ -37,53 +37,6 @@ const (
const DockerHostFromEnv = "$DOCKER_HOST" const DockerHostFromEnv = "$DOCKER_HOST"
var WellKnownHTTPPorts = map[uint16]bool{
80: true,
8000: true,
8008: true,
8080: true,
3000: true,
}
var (
ServiceNamePortMapTCP = map[string]int{
"postgres": 5432,
"mysql": 3306,
"mariadb": 3306,
"redis": 6379,
"mssql": 1433,
"memcached": 11211,
"rabbitmq": 5672,
"mongo": 27017,
"minecraft-server": 25565,
"dns": 53,
"ssh": 22,
"ftp": 21,
"smtp": 25,
"pop3": 110,
"imap": 143,
}
)
var ImageNamePortMapHTTP = map[string]int{
"nginx": 80,
"httpd": 80,
"adguardhome": 3000,
"gogs": 3000,
"gitea": 3000,
"portainer": 9000,
"portainer-ce": 9000,
"home-assistant": 8123,
"homebridge": 8581,
"uptime-kuma": 3001,
"changedetection.io": 3000,
"prometheus": 9090,
"grafana": 3000,
"dockge": 5001,
"nginx-proxy-manager": 81,
}
const ( const (
IdleTimeoutDefault = "0" IdleTimeoutDefault = "0"
WakeTimeoutDefault = "10s" WakeTimeoutDefault = "10s"

74
src/common/ports.go Normal file
View file

@ -0,0 +1,74 @@
package common
var (
WellKnownHTTPPorts = map[string]bool{
"80": true,
"8000": true,
"8008": true,
"8080": true,
"3000": true,
}
ServiceNamePortMapTCP = map[string]int{
"mssql": 1433,
"mysql": 3306,
"mariadb": 3306,
"postgres": 5432,
"rabbitmq": 5672,
"redis": 6379,
"memcached": 11211,
"mongo": 27017,
"ssh": 22,
"ftp": 21,
"smtp": 25,
"dns": 53,
"pop3": 110,
"imap": 143,
}
ImageNamePortMap = func() (m map[string]int) {
m = make(map[string]int, len(ServiceNamePortMapTCP)+len(imageNamePortMap))
for k, v := range ServiceNamePortMapTCP {
m[k] = v
}
for k, v := range imageNamePortMap {
m[k] = v
}
return
}()
imageNamePortMap = map[string]int{
"adguardhome": 3000,
"bazarr": 6767,
"calibre-web": 8083,
"changedetection.io": 3000,
"dockge": 5001,
"gitea": 3000,
"gogs": 3000,
"grafana": 3000,
"home-assistant": 8123,
"homebridge": 8581,
"httpd": 80,
"immich": 3001,
"jellyfin": 8096,
"lidarr": 8686,
"minecraft-server": 25565,
"nginx": 80,
"nginx-proxy-manager": 81,
"open-webui": 8080,
"plex": 32400,
"portainer": 9000,
"portainer-ce": 9000,
"prometheus": 9090,
"prowlarr": 9696,
"radarr": 7878,
"radarr-sma": 7878,
"rsshub": 1200,
"rss-bridge": 80,
"sonarr": 8989,
"sonarr-sma": 8989,
"uptime-kuma": 3001,
"whisparr": 6969,
}
)

View file

@ -55,6 +55,8 @@ func (b Builder) WithSeverity(s Severity) Builder {
func (b Builder) Build() NestedError { func (b Builder) Build() NestedError {
if len(b.errors) == 0 { if len(b.errors) == 0 {
return nil return nil
} else if len(b.errors) == 1 {
return b.errors[0]
} }
return Join(b.message, b.errors...).Severity(b.severity) return Join(b.message, b.errors...).Severity(b.severity)
} }

View file

@ -43,10 +43,10 @@ func TestBuilderNested(t *testing.T) {
expected2 := expected2 :=
(`error occurred: (`error occurred:
- Action 1 failed: - Action 1 failed:
- invalid Inner: 2 - invalid Inner: "1"
- invalid Inner: 1 - invalid Inner: "2"
- Action 2 failed: - Action 2 failed:
- invalid Inner: 3`) - invalid Inner: "3"`)
if got != expected1 && got != expected2 { if got != expected1 && got != expected2 {
t.Errorf("expected \n%s, got \n%s", expected1, got) t.Errorf("expected \n%s, got \n%s", expected1, got)
} }

View file

@ -10,10 +10,12 @@ var (
ErrUnsupported = stderrors.New("unsupported") ErrUnsupported = stderrors.New("unsupported")
ErrUnexpected = stderrors.New("unexpected") ErrUnexpected = stderrors.New("unexpected")
ErrNotExists = stderrors.New("does not exist") ErrNotExists = stderrors.New("does not exist")
ErrMissing = stderrors.New("missing")
ErrAlreadyExist = stderrors.New("already exist") ErrAlreadyExist = stderrors.New("already exist")
ErrOutOfRange = stderrors.New("out of range")
) )
const fmtSubjectWhat = "%w %v: %v" const fmtSubjectWhat = "%w %v: %q"
func Failure(what string) NestedError { func Failure(what string) NestedError {
return errorf("%s %w", what, ErrFailure) return errorf("%s %w", what, ErrFailure)
@ -47,6 +49,14 @@ func NotExist(subject, what any) NestedError {
return errorf("%v %w: %v", subject, ErrNotExists, what) return errorf("%v %w: %v", subject, ErrNotExists, what)
} }
func Missing(subject any) NestedError {
return errorf("%w %v", ErrMissing, subject)
}
func AlreadyExist(subject, what any) NestedError { func AlreadyExist(subject, what any) NestedError {
return errorf("%v %w: %v", subject, ErrAlreadyExist, what) return errorf("%v %w: %v", subject, ErrAlreadyExist, what)
} }
func OutOfRange(subject string, value any) NestedError {
return errorf("%v %w: %v", subject, ErrOutOfRange, value)
}

View file

@ -31,50 +31,55 @@ type (
var NewProxyEntries = F.NewMapOf[string, *RawEntry] var NewProxyEntries = F.NewMapOf[string, *RawEntry]
func (e *RawEntry) SetDefaults() { func (e *RawEntry) FillMissingFields() bool {
if e.ProxyProperties == nil { isDocker := e.ProxyProperties != nil
if !isDocker {
e.ProxyProperties = &D.ProxyProperties{} e.ProxyProperties = &D.ProxyProperties{}
} }
if e.Scheme == "" { if e.Port == "" {
if port, ok := ServiceNamePortMapTCP[e.ImageName]; ok {
e.Port = strconv.Itoa(port)
} else if port, ok := ImageNamePortMap[e.ImageName]; ok {
e.Port = strconv.Itoa(port)
} else {
switch { switch {
case strings.ContainsRune(e.Port, ':'): case e.Scheme == "https":
e.Scheme = "tcp" e.Port = "443"
case e.ProxyProperties != nil: case !isDocker:
if _, ok := ServiceNamePortMapTCP[e.ImageName]; ok { e.Port = "80"
e.Scheme = "tcp"
} }
} }
} }
if e.Port == "" {
if e.FirstPort == "" {
return false
}
e.Port = e.FirstPort
}
if e.Scheme == "" { if e.Scheme == "" {
switch e.Port { if _, ok := ServiceNamePortMapTCP[e.ImageName]; ok {
case "443", "8443": e.Scheme = "tcp"
} else if strings.ContainsRune(e.Port, ':') {
e.Scheme = "tcp"
} else if _, ok := WellKnownHTTPPorts[e.Port]; ok {
e.Scheme = "http"
} else if e.Port == "443" {
e.Scheme = "https" e.Scheme = "https"
default: } else if isDocker && e.Port == "" {
return false
} else {
e.Scheme = "http" e.Scheme = "http"
} }
} }
if e.Host == "" { if e.Host == "" {
e.Host = "localhost" e.Host = "localhost"
} }
if e.Port == "" {
e.Port = e.FirstPort
}
if e.Port == "" {
if port, ok := ServiceNamePortMapTCP[e.Port]; ok {
e.Port = strconv.Itoa(port)
} else if port, ok := ImageNamePortMapHTTP[e.Port]; ok {
e.Port = strconv.Itoa(port)
} else {
switch e.Scheme {
case "http":
e.Port = "80"
case "https":
e.Port = "443"
}
}
}
if e.IdleTimeout == "" { if e.IdleTimeout == "" {
e.IdleTimeout = IdleTimeoutDefault e.IdleTimeout = IdleTimeoutDefault
} }
@ -87,4 +92,6 @@ func (e *RawEntry) SetDefaults() {
if e.StopMethod == "" { if e.StopMethod == "" {
e.StopMethod = StopMethodDefault e.StopMethod = StopMethodDefault
} }
return true
} }

View file

@ -44,7 +44,10 @@ func (rp *ReverseProxyEntry) UseIdleWatcher() bool {
} }
func ValidateEntry(m *M.RawEntry) (any, E.NestedError) { func ValidateEntry(m *M.RawEntry) (any, E.NestedError) {
m.SetDefaults() if !m.FillMissingFields() {
return nil, E.Missing("fields")
}
scheme, err := T.NewScheme(m.Scheme) scheme, err := T.NewScheme(m.Scheme)
if err.HasError() { if err.HasError() {
return nil, err return nil, err

View file

@ -13,7 +13,7 @@ func NewPathPattern(s string) (PathPattern, E.NestedError) {
if len(s) == 0 { if len(s) == 0 {
return "", E.Invalid("path", "must not be empty") return "", E.Invalid("path", "must not be empty")
} }
if !pathPattern.MatchString(string(s)) { if !pathPattern.MatchString(s) {
return "", E.Invalid("path pattern", s) return "", E.Invalid("path pattern", s)
} }
return PathPattern(s), nil return PathPattern(s), nil
@ -34,4 +34,4 @@ func ValidatePathPatterns(s []string) (PathPatterns, E.NestedError) {
return pp, nil return pp, nil
} }
var pathPattern = regexp.MustCompile("^((GET|POST|DELETE|PUT|PATCH|HEAD|OPTIONS|CONNECT)\\s)?(/\\w*)+/?$") var pathPattern = regexp.MustCompile(`^(/[-\w./]*({\$\})?|((GET|POST|DELETE|PUT|HEAD|OPTION) /[-\w./]*({\$\})?))$`)

View file

@ -0,0 +1,47 @@
package fields
import (
"testing"
E "github.com/yusing/go-proxy/error"
U "github.com/yusing/go-proxy/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 := NewPathPattern(pattern)
U.ExpectNoError(t, err.Error())
}
for _, pattern := range invalidPatterns {
_, err := NewPathPattern(pattern)
U.ExpectError2(t, pattern, E.ErrInvalid, err.Error())
}
}

View file

@ -13,22 +13,19 @@ func ValidatePort(v string) (Port, E.NestedError) {
if err != nil { if err != nil {
return ErrPort, E.Invalid("port number", v).With(err) return ErrPort, E.Invalid("port number", v).With(err)
} }
return NewPortInt(p) return ValidatePortInt(p)
} }
func NewPortInt[Int int | uint16](v Int) (Port, E.NestedError) { func ValidatePortInt[Int int | uint16](v Int) (Port, E.NestedError) {
pp := Port(v) p := Port(v)
if err := pp.boundCheck(); err.HasError() { if !p.inBound() {
return ErrPort, err return ErrPort, E.OutOfRange("port", p)
} }
return pp, nil return p, nil
} }
func (p Port) boundCheck() E.NestedError { func (p Port) inBound() bool {
if p < MinPort || p > MaxPort { return p >= MinPort && p <= MaxPort
return E.Invalid("port", p)
}
return nil
} }
const ( const (

View file

@ -1,7 +1,6 @@
package fields package fields
import ( import (
"fmt"
"strings" "strings"
"github.com/yusing/go-proxy/common" "github.com/yusing/go-proxy/common"
@ -15,36 +14,42 @@ type StreamPort struct {
func ValidateStreamPort(p string) (StreamPort, E.NestedError) { func ValidateStreamPort(p string) (StreamPort, E.NestedError) {
split := strings.Split(p, ":") split := strings.Split(p, ":")
if len(split) != 2 {
return StreamPort{}, E.Invalid("stream port", fmt.Sprintf("%q", p)).With("should be in 'x:y' format") switch len(split) {
case 1:
split = []string{"0", split[0]}
case 2:
break
default:
return ErrStreamPort, E.Invalid("stream port", p).With("too many colons")
} }
listeningPort, err := ValidatePort(split[0]) listeningPort, err := ValidatePort(split[0])
if err.HasError() { if err != nil {
return StreamPort{}, err return ErrStreamPort, err
}
if err = listeningPort.boundCheck(); err.HasError() {
return StreamPort{}, err
} }
proxyPort, err := ValidatePort(split[1]) proxyPort, err := ValidatePort(split[1])
if err.HasError() { if err.Is(E.ErrOutOfRange) {
return ErrStreamPort, err
} else if proxyPort == 0 {
return ErrStreamPort, E.Invalid("stream port", p).With("proxy port cannot be 0")
} else if err != nil {
proxyPort, err = parseNameToPort(split[1]) proxyPort, err = parseNameToPort(split[1])
if err.HasError() { if err != nil {
return StreamPort{}, err return ErrStreamPort, err
} }
} }
if err = proxyPort.boundCheck(); err.HasError() {
return StreamPort{}, err
}
return StreamPort{ListeningPort: listeningPort, ProxyPort: proxyPort}, nil return StreamPort{listeningPort, proxyPort}, nil
} }
func parseNameToPort(name string) (Port, E.NestedError) { func parseNameToPort(name string) (Port, E.NestedError) {
port, ok := common.ServiceNamePortMapTCP[name] port, ok := common.ServiceNamePortMapTCP[name]
if !ok { if !ok {
return -1, E.Unsupported("service", name) return ErrPort, E.Invalid("service", name)
} }
return Port(port), nil return Port(port), nil
} }
var ErrStreamPort = StreamPort{ErrPort, ErrPort}

View file

@ -0,0 +1,48 @@
package fields
import (
"testing"
E "github.com/yusing/go-proxy/error"
U "github.com/yusing/go-proxy/utils/testing"
)
var validPorts = []string{
"1234:5678",
"0:2345",
"2345",
"1234:postgres",
}
var invalidPorts = []string{
"",
"123:",
"0:",
":1234",
"1234:1234:1234",
"qwerty",
"asdfgh:asdfgh",
"1234:asdfgh",
}
var outOfRangePorts = []string{
"-1:1234",
"1234:-1",
"65536",
"0:65536",
}
func TestStreamPort(t *testing.T) {
for _, port := range validPorts {
_, err := ValidateStreamPort(port)
U.ExpectNoError(t, err.Error())
}
for _, port := range invalidPorts {
_, err := ValidateStreamPort(port)
U.ExpectError2(t, port, E.ErrInvalid, err.Error())
}
for _, port := range outOfRangePorts {
_, err := ValidateStreamPort(port)
U.ExpectError2(t, port, E.ErrOutOfRange, err.Error())
}
}

View file

@ -137,6 +137,11 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.Ra
errors.Add(p.applyLabel(container, entries, key, val)) errors.Add(p.applyLabel(container, entries, key, val))
} }
// remove all entries that failed to fill in missing fields
entries.RemoveAll(func(re *M.RawEntry) bool {
return !re.FillMissingFields()
})
// selecting correct host port // selecting correct host port
if container.HostConfig.NetworkMode != "host" { if container.HostConfig.NetworkMode != "host" {
for _, a := range container.Aliases { for _, a := range container.Aliases {
@ -148,12 +153,12 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.Ra
containerPort := strconv.Itoa(int(p.PrivatePort)) containerPort := strconv.Itoa(int(p.PrivatePort))
publicPort := strconv.Itoa(int(p.PublicPort)) publicPort := strconv.Itoa(int(p.PublicPort))
entryPortSplit := strings.Split(entry.Port, ":") entryPortSplit := strings.Split(entry.Port, ":")
if len(entryPortSplit) == 2 && entryPortSplit[1] == containerPort { n := len(entryPortSplit)
entryPortSplit[1] = publicPort if entryPortSplit[n-1] == containerPort {
} else if len(entryPortSplit) == 1 && entryPortSplit[0] == containerPort { entryPortSplit[n-1] = publicPort
entryPortSplit[0] = publicPort
}
entry.Port = strings.Join(entryPortSplit, ":") entry.Port = strings.Join(entryPortSplit, ":")
break
}
} }
} }
} }

View file

@ -115,6 +115,7 @@ func TestApplyLabel(t *testing.T) {
Labels: map[string]string{ Labels: map[string]string{
D.LableAliases: "a,b,c", D.LableAliases: "a,b,c",
"proxy.a.no_tls_verify": "true", "proxy.a.no_tls_verify": "true",
"proxy.a.port": "3333",
"proxy.b.port": "1234", "proxy.b.port": "1234",
"proxy.c.scheme": "https", "proxy.c.scheme": "https",
}}, "") }}, "")
@ -132,6 +133,7 @@ func TestApplyLabelWithRef(t *testing.T) {
Labels: map[string]string{ Labels: map[string]string{
D.LableAliases: "a,b,c", D.LableAliases: "a,b,c",
"proxy.$1.host": "localhost", "proxy.$1.host": "localhost",
"proxy.$1.port": "4444",
"proxy.$2.port": "1234", "proxy.$2.port": "1234",
"proxy.$3.scheme": "https", "proxy.$3.scheme": "https",
}}, "") }}, "")

View file

@ -42,7 +42,7 @@ var NewRoutes = F.NewMapOf[string, Route]
func NewRoute(en *M.RawEntry) (Route, E.NestedError) { func NewRoute(en *M.RawEntry) (Route, E.NestedError) {
rt, err := P.ValidateEntry(en) rt, err := P.ValidateEntry(en)
if err.HasError() { if err != nil {
return nil, err return nil, err
} }

View file

@ -16,7 +16,14 @@ func ExpectNoError(t *testing.T, err error) {
func ExpectError(t *testing.T, expected error, err error) { func ExpectError(t *testing.T, expected error, err error) {
t.Helper() t.Helper()
if !errors.Is(err, expected) { if !errors.Is(err, expected) {
t.Errorf("expected err %s, got nil", expected.Error()) t.Errorf("expected err %s, got %s", expected.Error(), err.Error())
}
}
func ExpectError2(t *testing.T, input any, expected error, err error) {
t.Helper()
if !errors.Is(err, expected) {
t.Errorf("%v: expected err %s, got %s", input, expected.Error(), err.Error())
} }
} }